From 353438dbd300641e125cd52a300c9a261a03a69f Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 4 Feb 2021 01:34:12 -0500 Subject: [PATCH] feat: allow builders to be detected based on repository contents Rather than hardcode two builders, allow builders to specify a `builder-detect` trigger. This trigger can be used to specify if the builder should or should not be used for an application. Each builder takes stdin and can decide if it wants to emit it or emit it's own image source type. If the final value is empty, then Dokku will default to herokuish (and cnb once that is stable). In addition, a future change may allow users to manually specify a builder in the case they wish to override the choice selected by Dokku. This change enables users to build custom builder plugins and have those plugins used for building an image asset. By way of example, an enterprising user could create a `builder-lambda` based on lambci, and then pair this with a scheduler plugin that updates a lambda function on AWS. Alternatively, a user might decide they wish to place their Dockerfile in a specific directory for their applications - such as an `_infrastructure` directory - and create a plugin to override how that is detected within Dokku. --- docs/development/plugin-triggers.md | 18 ++++ plugins/20_events/builder-detect | 1 + plugins/builder-cnb/builder-detect | 22 ++++ plugins/builder-dockerfile/builder-detect | 30 ++++++ plugins/builder-herokuish/builder-detect | 27 +++++ plugins/builder-herokuish/functions | 38 +++++++ plugins/git/functions | 17 ++- plugins/tar/functions | 17 ++- tests/unit/builder.bats | 121 ++++++++++++++++++++++ 9 files changed, 271 insertions(+), 20 deletions(-) create mode 120000 plugins/20_events/builder-detect create mode 100755 plugins/builder-cnb/builder-detect create mode 100755 plugins/builder-dockerfile/builder-detect create mode 100755 plugins/builder-herokuish/builder-detect create mode 100755 plugins/builder-herokuish/functions create mode 100644 tests/unit/builder.bats diff --git a/docs/development/plugin-triggers.md b/docs/development/plugin-triggers.md index 61d05c603..fa8505b9b 100644 --- a/docs/development/plugin-triggers.md +++ b/docs/development/plugin-triggers.md @@ -143,6 +143,24 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x # TODO ``` +### `builder-detect` + +- Description: Allows overriding the auto-detected `herokuish` builder in favor of a custom one. Dockerfile gets lowest builder precedence. +- Invoked by: `dokku deploy` +- Arguments: `$APP` `$SOURCECODE_WORK_DIR` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +APP="$1"; SOURCECODE_WORK_DIR="$2" + +if [[ -f "$SOURCECODE_WORK_DIR/project.toml" ]]; then + echo -n "cnb" +fi +``` + ### `builder-create-dokku-image` - Description: Allows modification of the configured dokku-image diff --git a/plugins/20_events/builder-detect b/plugins/20_events/builder-detect new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/builder-detect @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/builder-cnb/builder-detect b/plugins/builder-cnb/builder-detect new file mode 100755 index 000000000..65678e55d --- /dev/null +++ b/plugins/builder-cnb/builder-detect @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/config/functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-builder-cnb-builder-detect() { + declare desc="builder-cnb builder-detect plugin trigger" + declare trigger="builder-detect" + declare APP="$1" SOURCECODE_WORK_DIR="$2" + + if [[ -f "$SOURCECODE_WORK_DIR/project.toml" ]]; then + echo "cnb" + return + fi + + if [[ "$(config_get "$APP" DOKKU_CNB_EXPERIMENTAL || true)" == "1" ]]; then + echo "cnb" + return + fi +} + +trigger-builder-cnb-builder-detect "$@" diff --git a/plugins/builder-dockerfile/builder-detect b/plugins/builder-dockerfile/builder-detect new file mode 100755 index 000000000..1def88e9f --- /dev/null +++ b/plugins/builder-dockerfile/builder-detect @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/config/functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-builder-dockerfile-builder-detect() { + declare desc="builder-dockerfile builder-detect plugin trigger" + declare trigger="builder-detect" + declare APP="$1" SOURCECODE_WORK_DIR="$2" + + # buildpacks always win against dockerfile detection + # that includes cnb + if [[ -f "$SOURCECODE_WORK_DIR/project.toml" ]]; then + return + fi + + # buildpacks always win against dockerfile detection + # that includes cnb + if [[ "$(config_get "$APP" DOKKU_CNB_EXPERIMENTAL || true)" == "1" ]]; then + echo "cnb" + return + fi + + if [[ -f "$SOURCECODE_WORK_DIR/Dockerfile" ]]; then + echo "dockerfile" + return + fi +} + +trigger-builder-dockerfile-builder-detect "$@" diff --git a/plugins/builder-herokuish/builder-detect b/plugins/builder-herokuish/builder-detect new file mode 100755 index 000000000..7ee618691 --- /dev/null +++ b/plugins/builder-herokuish/builder-detect @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/builder-herokuish/functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-builder-herokuish-builder-detect() { + declare desc="builder-herokuish builder-detect plugin trigger" + declare trigger="builder-detect" + declare APP="$1" SOURCECODE_WORK_DIR="$2" + + if fn-has-buildpacks-file "$SOURCECODE_WORK_DIR"; then + echo "herokuish" + return + fi + + if fn-has-buildpack-dotenv "$SOURCECODE_WORK_DIR"; then + echo "herokuish" + return + fi + + if fn-has-buildpack-env "$APP"; then + echo "herokuish" + return + fi +} + +trigger-builder-herokuish-builder-detect "$@" diff --git a/plugins/builder-herokuish/functions b/plugins/builder-herokuish/functions new file mode 100755 index 000000000..8452214a1 --- /dev/null +++ b/plugins/builder-herokuish/functions @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/config/functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +fn-has-buildpacks-file() { + declare SOURCECODE_WORK_DIR="$1" + + if [[ -f "$SOURCECODE_WORK_DIR/.buildpacks" ]]; then + return 0 + fi + + return 1 +} + +fn-has-buildpack-dotenv() { + declare SOURCECODE_WORK_DIR="$1" + + if [[ ! -f "$SOURCECODE_WORK_DIR/.env" ]]; then + return 1 + fi + + if grep -q BUILDPACK_URL "$SOURCECODE_WORK_DIR/.env"; then + return 0 + fi + + return 1 +} + +fn-has-buildpack-env() { + declare APP="$1" + + if [[ -n $(config_get "$APP" BUILDPACK_URL || true) ]]; then + return 0 + fi + + return 1 +} diff --git a/plugins/git/functions b/plugins/git/functions index 114c28971..a67b795e1 100755 --- a/plugins/git/functions +++ b/plugins/git/functions @@ -49,18 +49,15 @@ git_build_app_repo() { git_trigger_build() { declare desc="triggers the actual build process for a given app within a directory at a particular revision" declare APP="$1" TMP_WORK_DIR="$2" REV="$3" + local BUILDER plugn trigger post-extract "$APP" "$TMP_WORK_DIR" "$REV" - if [[ -f Dockerfile ]] && [[ "$( - [[ -f .env ]] && grep -q BUILDPACK_URL .env - echo $? - )" != "0" ]] && [[ ! -f ".buildpacks" ]] && [[ -z $(config_get "$APP" BUILDPACK_URL || true) ]]; then - plugn trigger pre-receive-app "$APP" "dockerfile" "$TMP_WORK_DIR" "$REV" - dokku_receive "$APP" "dockerfile" "$TMP_WORK_DIR" - else - plugn trigger pre-receive-app "$APP" "herokuish" "$TMP_WORK_DIR" "$REV" - dokku_receive "$APP" "herokuish" "$TMP_WORK_DIR" - fi + + BUILDER="$(plugn trigger builder-detect "$APP" "$TMP_WORK_DIR" | tail -n1 || true)" + [[ -z "$BUILDER" ]] && BUILDER="herokuish" + + plugn trigger pre-receive-app "$APP" "$BUILDER" "$TMP_WORK_DIR" "$REV" + dokku_receive "$APP" "$BUILDER" "$TMP_WORK_DIR" } git_deploy_branch() { diff --git a/plugins/tar/functions b/plugins/tar/functions index 9a8302c99..c95d29303 100755 --- a/plugins/tar/functions +++ b/plugins/tar/functions @@ -41,18 +41,15 @@ tar_build() { tar_trigger_build() { declare desc="triggers the actual build process for a given app within a directory at a particular revision" declare APP="$1" TMP_WORK_DIR="$2" REV="$3" + local BUILDER plugn trigger post-extract "$APP" "$TMP_WORK_DIR" "$REV" - if [[ -f Dockerfile ]] && [[ "$( - [[ -f .env ]] && grep -q BUILDPACK_URL .env - echo $? - )" != "0" ]] && [[ ! -f ".buildpacks" ]] && [[ -z $(config_get "$APP" BUILDPACK_URL || true) ]]; then - plugn trigger pre-receive-app "$APP" "dockerfile" "$TMP_WORK_DIR" "$REV" - dokku_receive "$APP" "dockerfile" "$TMP_WORK_DIR" - else - plugn trigger pre-receive-app "$APP" "herokuish" "$TMP_WORK_DIR" "$REV" - dokku_receive "$APP" "herokuish" "$TMP_WORK_DIR" - fi + + BUILDER="$(plugn trigger builder-detect "$APP" "$TMP_WORK_DIR" | tail -n1 || true)" + [[ -z "$BUILDER" ]] && BUILDER="herokuish" + + plugn trigger pre-receive-app "$APP" "$BUILDER" "$TMP_WORK_DIR" "$REV" + dokku_receive "$APP" "$BUILDER" "$TMP_WORK_DIR" } cmd-tar-in() { diff --git a/tests/unit/builder.bats b/tests/unit/builder.bats new file mode 100644 index 000000000..d6f620c4e --- /dev/null +++ b/tests/unit/builder.bats @@ -0,0 +1,121 @@ +#!/usr/bin/env bats + +load test_helper + +setup() { + create_app +} + +teardown() { + destroy_app +} + +@test "(builder) builder-detect [cnb]" { + local TMP=$(mktemp -d "/tmp/dokku.me.XXXXX") + trap 'popd &>/dev/null || true; rm -rf "$TMP"' INT TERM + + # test project.toml + run touch "$TMP/project.toml" + echo "output: $output" + echo "status: $status" + assert_success + + chown -R dokku:dokku "$TMP" + run /bin/bash -c "dokku plugin:trigger builder-detect $TEST_APP $TMP | tail -n1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "cnb" + + sudo rm -rf $TMP/* + echo "output: $output" + echo "status: $status" + assert_success + + # test DOKKU_CNB_EXPERIMENTAL env var + run /bin/bash -c "dokku config:set $TEST_APP DOKKU_CNB_EXPERIMENTAL=1" + echo "output: $output" + echo "status: $status" + assert_success + + chown -R dokku:dokku "$TMP" + run /bin/bash -c "dokku plugin:trigger builder-detect $TEST_APP $TMP | tail -n1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "cnb" +} + +@test "(builder) builder-detect [dockerfile]" { + local TMP=$(mktemp -d "/tmp/dokku.me.XXXXX") + trap 'popd &>/dev/null || true; rm -rf "$TMP"' INT TERM + + run touch "$TMP/Dockerfile" + echo "output: $output" + echo "status: $status" + assert_success + + chown -R dokku:dokku "$TMP" + run /bin/bash -c "dokku plugin:trigger builder-detect $TEST_APP $TMP | tail -n1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "dockerfile" +} + +@test "(builder) builder-detect [herokuish]" { + local TMP=$(mktemp -d "/tmp/dokku.me.XXXXX") + trap 'popd &>/dev/null || true; rm -rf "$TMP"' INT TERM + + touch "$TMP/Dockerfile" + + # test buildpacks + chown -R dokku:dokku "$TMP" + run /bin/bash -c "touch $TMP/.buildpacks" + echo "output: $output" + echo "status: $status" + assert_success + + chown -R dokku:dokku "$TMP" + run /bin/bash -c "dokku plugin:trigger builder-detect $TEST_APP $TMP | tail -n1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "herokuish" + + sudo rm -rf $TMP/* + echo "output: $output" + echo "status: $status" + assert_success + + # test .env + run /bin/bash -c "echo BUILDPACK_URL=null > $TMP/.env" + echo "output: $output" + echo "status: $status" + assert_success + + chown -R dokku:dokku "$TMP" + run /bin/bash -c "dokku plugin:trigger builder-detect $TEST_APP $TMP | tail -n1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "herokuish" + + sudo rm -rf $TMP/* + echo "output: $output" + echo "status: $status" + assert_success + + # test BUILDPACK_URL env var + run /bin/bash -c "dokku config:set $TEST_APP BUILDPACK_URL=null" + echo "output: $output" + echo "status: $status" + assert_success + + chown -R dokku:dokku "$TMP" + run /bin/bash -c "dokku plugin:trigger builder-detect $TEST_APP $TMP | tail -n1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "herokuish" +}