refactor: move herokuish app cache from the filesystem into a docker volume

This simplifies cache referencing and also paves the way for removing more from the app git directory.

Refs #1558
This commit is contained in:
Jose Diaz-Gonzalez
2022-11-28 02:24:27 -05:00
parent ce0177967d
commit 40b20e0c08
13 changed files with 126 additions and 50 deletions

View File

@@ -0,0 +1,5 @@
# 0.30.0 Migration Guide
## Changes
- Herokuish build cache is now mounted from a docker volume - eg. `cache-node-js-app` - instead of the local filesystem. All existing app cache will be cleared upon upgrading past 0.29.0.

View File

@@ -18,6 +18,7 @@ Docker releases updates periodically to their engine. We recommend reading their
Before upgrading, check the migration guides to get comfortable with new features and prepare your deployment to be upgraded.
- [Upgrading to 0.31](/docs/appendices/0.31.0-migration-guide.md)
- [Upgrading to 0.30](/docs/appendices/0.30.0-migration-guide.md)
- [Upgrading to 0.29](/docs/appendices/0.29.0-migration-guide.md)
- [Upgrading to 0.28](/docs/appendices/0.28.0-migration-guide.md)

View File

@@ -3,6 +3,16 @@ set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_AVAILABLE_PATH/config/functions"
fn-builder-herokuish-ensure-cache() {
declare APP="$1"
existing_cache="$("$DOCKER_BIN" volume ls --quiet --filter label=com.dokku.app-name=test --filter label=com.dokku.builder-type=herokuish)"
if [[ "$existing_cache" != "cache-$APP" ]]; then
"$DOCKER_BIN" volume rm "cache-$APP" >/dev/null 2>&1 || true
"$DOCKER_BIN" volume create "--label=org.label-schema.schema-version=1.0" "--label=org.label-schema.vendor=dokku" "--label=com.dokku.app-name=$APP" "--label=com.dokku.builder-type=herokuish" "cache-$APP" >/dev/null
fi
}
trigger-builder-herokuish-builder-build() {
declare desc="builder-herokuish builder-build plugin trigger"
declare trigger="builder-build"
@@ -21,8 +31,6 @@ trigger-builder-herokuish-builder-build() {
fi
local IMAGE=$(get_app_image_name "$APP")
local DOKKU_APP_CACHE_DIR="$DOKKU_ROOT/$APP/cache"
local DOKKU_APP_HOST_CACHE_DIR="$DOKKU_HOST_ROOT/$APP/cache"
local DOCKER_COMMIT_LABEL_ARGS=("--change" "LABEL dokku=" "--change" "LABEL org.label-schema.schema-version=1.0" "--change" "LABEL org.label-schema.vendor=dokku" "--change" "LABEL com.dokku.image-stage=build" "--change" "LABEL com.dokku.builder-type=herokuish" "--change" "LABEL com.dokku.app-name=$APP")
local DOCKER_RUN_LABEL_ARGS=("--label=dokku" "--label=org.label-schema.schema-version=1.0" "--label=org.label-schema.vendor=dokku" "--label=com.dokku.image-stage=build" "--label=com.dokku.builder-type=herokuish" "--label=com.dokku.app-name=$APP")
local CID TAR_CID
@@ -49,7 +57,6 @@ trigger-builder-herokuish-builder-build() {
"$DOCKER_BIN" container commit "${DOCKER_COMMIT_LABEL_ARGS[@]}" "$TAR_CID" "$IMAGE" >/dev/null
DOKKU_SKIP_IMAGE_CLEANUP_REGISTRATION=1 plugn trigger scheduler-register-retired "$APP" "$TAR_CID"
[[ -d $DOKKU_APP_CACHE_DIR ]] || mkdir -p "$DOKKU_APP_CACHE_DIR"
plugn trigger pre-build-buildpack "$APP" "$SOURCECODE_WORK_DIR"
local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$BUILDER_TYPE")
@@ -60,8 +67,8 @@ trigger-builder-herokuish-builder-build() {
eval "ARG_ARRAY=($DOCKER_ARGS)"
local DOKKU_CONTAINER_EXIT_CODE=0
if ! CID=$("$DOCKER_BIN" container create "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS -v "$DOKKU_APP_HOST_CACHE_DIR:/cache" --env=CACHE_PATH=/cache "${ARG_ARRAY[@]}" "$IMAGE" /build); then
DOKKU_SKIP_IMAGE_CLEANUP_REGISTRATION=1 plugn trigger scheduler-register-retired "$APP" "$TAR_CID"
fn-builder-herokuish-ensure-cache "$APP"
if ! CID=$("$DOCKER_BIN" container create "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS -v "cache-$APP:/cache" --env=CACHE_PATH=/cache "${ARG_ARRAY[@]}" "$IMAGE" /build); then
plugn trigger scheduler-register-retired "$APP" "$CID"
dokku_log_warn "Failure during app build"
return 1
@@ -70,7 +77,6 @@ trigger-builder-herokuish-builder-build() {
plugn trigger post-container-create "app" "$CID" "$APP" "build"
"$DOCKER_BIN" container start "$CID" >/dev/null || DOKKU_CONTAINER_EXIT_CODE=$?
if ! "$DOCKER_BIN" container attach "$CID"; then
DOKKU_SKIP_IMAGE_CLEANUP_REGISTRATION=1 plugn trigger scheduler-register-retired "$APP" "$TAR_CID"
plugn trigger scheduler-register-retired "$APP" "$CID"
dokku_log_warn "Failure during app build"
return 1
@@ -78,14 +84,12 @@ trigger-builder-herokuish-builder-build() {
DOKKU_CONTAINER_EXIT_CODE="$("$DOCKER_BIN" container wait "$CID" 2>/dev/null || echo "$DOKKU_CONTAINER_EXIT_CODE")"
if [[ "$DOKKU_CONTAINER_EXIT_CODE" -ne 0 ]]; then
DOKKU_SKIP_IMAGE_CLEANUP_REGISTRATION=1 plugn trigger scheduler-register-retired "$APP" "$TAR_CID"
plugn trigger scheduler-register-retired "$APP" "$CID"
dokku_log_warn "Failure during app build"
return 1
fi
"$DOCKER_BIN" container commit "${DOCKER_COMMIT_LABEL_ARGS[@]}" "$CID" "$IMAGE" >/dev/null
DOKKU_SKIP_IMAGE_CLEANUP_REGISTRATION=1 plugn trigger scheduler-register-retired "$APP" "$TAR_CID"
plugn trigger scheduler-register-retired "$APP" "$CID"
plugn trigger post-build-buildpack "$APP" "$SOURCECODE_WORK_DIR"
}

View File

@@ -1,13 +1,28 @@
#!/usr/bin/env bash
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
fn-builder-herokuish-remove-old-cache() {
for app in $(dokku_apps "false" 2>/dev/null); do
local DOKKU_APP_CACHE_DIR="$DOKKU_ROOT/$APP/cache"
local DOKKU_APP_HOST_CACHE_DIR="$DOKKU_HOST_ROOT/$APP/cache"
if [[ ! -d "$DOKKU_APP_CACHE_DIR" ]]; then
continue
fi
"$DOCKER_BIN" container run --rm --label=dokku --label=org.label-schema.schema-version=1.0 --label=org.label-schema.vendor=dokku "--label=com.dokku.app-name=$APP" -v "$DOKKU_APP_HOST_CACHE_DIR:/cache" "$DOKKU_IMAGE" "find /cache -depth -mindepth 1 -maxdepth 1 -exec rm -Rf {} ;"
rm -rf "$DOKKU_APP_CACHE_DIR"
done
}
trigger-builder-herokuish-install() {
declare desc="installs the builder-herokuish plugin"
declare trigger="install"
fn-plugin-property-setup "builder-herokuish"
fn-builder-herokuish-remove-old-cache
}
trigger-builder-herokuish-install "$@"

View File

@@ -203,7 +203,7 @@ func DockerCleanup(appName string, forceCleanup bool) error {
containerIDs := append(exitedContainerIDs, deadContainerIDs...)
if len(containerIDs) > 0 {
removeContainers(containerIDs)
DockerRemoveContainers(containerIDs)
}
// delete dangling images
@@ -351,21 +351,18 @@ func VerifyImage(image string) bool {
return imageCmd.Execute()
}
func listContainers(status string, appName string) ([]string, error) {
// DockerFilterContainers returns a slice of container IDs based on the passed in filters
func DockerFilterContainers(filters []string) ([]string, error) {
command := []string{
DockerBin(),
"container",
"list",
"--quiet",
"--all",
"--filter",
fmt.Sprintf("status=%v", status),
"--filter",
fmt.Sprintf("label=%v", os.Getenv("DOKKU_CONTAINER_LABEL")),
}
if appName != "" {
command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...)
for _, filter := range filters {
command = append(command, "--filter", filter)
}
var stderr bytes.Buffer
@@ -382,6 +379,18 @@ func listContainers(status string, appName string) ([]string, error) {
return output, nil
}
func listContainers(status string, appName string) ([]string, error) {
filters := []string{
fmt.Sprintf("status=%v", status),
fmt.Sprintf("label=%v", os.Getenv("DOKKU_CONTAINER_LABEL")),
}
if appName != "" {
filters = append(filters, fmt.Sprintf("label=com.dokku.app-name=%v", appName))
}
return DockerFilterContainers(filters)
}
func pruneUnusedImages(appName string) {
command := []string{
DockerBin(),
@@ -400,7 +409,8 @@ func pruneUnusedImages(appName string) {
pruneCmd.Execute()
}
func removeContainers(containerIDs []string) {
// DockerRemoveContainers will call `docker container rm` on the specified containers
func DockerRemoveContainers(containerIDs []string) {
command := []string{
DockerBin(),
"container",

View File

@@ -18,6 +18,7 @@ type ShellCmd struct {
CommandString string
Args []string
ShowOutput bool
ExitError *exec.ExitError
}
// NewShellCmd returns a new ShellCmd struct
@@ -57,6 +58,10 @@ func (sc *ShellCmd) Execute() bool {
sc.setup()
if err := sc.Command.Run(); err != nil {
exitError, ok := err.(*exec.ExitError)
if ok {
sc.ExitError = exitError
}
return false
}
return true

View File

@@ -2,5 +2,4 @@
/subcommands/*
/triggers/*
/triggers
/pre-delete
/post-stack-set
/post-*

View File

@@ -1,5 +1,5 @@
SUBCOMMANDS = subcommands/gc subcommands/purge-cache
TRIGGERS = triggers/post-stack-set triggers/pre-delete
TRIGGERS = triggers/post-stack-set triggers/post-delete
BUILD = commands subcommands triggers
PLUGIN_NAME = repo

View File

@@ -6,28 +6,45 @@ import (
"strings"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/config"
)
// PurgeCacheFailed wraps error to allow returning the correct exit code
type PurgeCacheFailed struct {
exitCode int
}
// ExitCode returns an exit code to use in case this error bubbles
// up into an os.Exit() call
func (err *PurgeCacheFailed) ExitCode() int {
return err.exitCode
}
// Error returns a standard non-existent app error
func (err *PurgeCacheFailed) Error() string {
return fmt.Sprintf("failed to purge cache, exit code %d", err.exitCode)
}
// PurgeCache deletes the contents of the build cache stored in the repository
func PurgeCache(appName string) error {
cacheDir := strings.Join([]string{common.AppRoot(appName), "cache"}, "/")
cacheHostDir := strings.Join([]string{common.AppHostRoot(appName), "cache"}, "/")
dokkuGlobalRunArgs := common.MustGetEnv("DOKKU_GLOBAL_RUN_ARGS")
image := config.GetWithDefault(appName, "DOKKU_IMAGE", os.Getenv("DOKKU_IMAGE"))
if info, _ := os.Stat(cacheDir); info != nil && info.IsDir() {
dockerLabelArgs := fmt.Sprintf("--label=com.dokku.app-name=%s", appName)
purgeCacheCmd := common.NewShellCmd(strings.Join([]string{
common.DockerBin(),
"container",
"run", "--rm", dockerLabelArgs, dokkuGlobalRunArgs,
"-v", strings.Join([]string{cacheHostDir, ":/cache"}, ""), image,
`find /cache -depth -mindepth 1 -maxdepth 1 -exec rm -Rf {} ;`}, " "))
purgeCacheCmd.Execute()
err := os.MkdirAll(cacheDir, 0644)
if err != nil {
return err
containerIDs, _ := common.DockerFilterContainers([]string{
fmt.Sprintf("label=com.dokku.app-name=%v", appName),
"label=com.dokku.image-stage=build",
})
if len(containerIDs) > 0 {
common.DockerRemoveContainers(containerIDs)
}
purgeCacheCmd := common.NewShellCmd(strings.Join([]string{
common.DockerBin(),
"volume",
"rm", "-f", fmt.Sprintf("cache-%s", appName)}, " "))
purgeCacheCmd.ShowOutput = false
purgeCacheCmd.Command.Stderr = os.Stderr
if !purgeCacheCmd.Execute() {
exitCode := 1
if purgeCacheCmd.ExitError != nil {
exitCode = purgeCacheCmd.ExitError.ExitCode()
}
return &PurgeCacheFailed{exitCode}
}
return nil

View File

@@ -21,7 +21,7 @@ func main() {
case "post-stack-set":
appName := flag.Arg(0)
err = repo.PurgeCache(appName)
case "pre-delete":
case "post-delete":
appName := flag.Arg(0)
err = repo.PurgeCache(appName)
default:

View File

@@ -59,12 +59,16 @@ teardown() {
}
@test "(build-env) buildpack deploy with Dockerfile" {
run /bin/bash -c "dokku config:set --no-restart $TEST_APP BUILDPACK_URL='https://github.com/heroku/heroku-buildpack-nodejs'"
run /bin/bash -c "dokku config:set --no-restart $TEST_APP BUILDPACK_URL='https://github.com/dokku/heroku-buildpack-null'"
echo "output: $output"
echo "status: $status"
assert_success
run deploy_app python dokku@dokku.me:$TEST_APP move_dockerfile_into_place
echo "output: $output"
echo "status: $status"
assert_success
deploy_app dockerfile
run /bin/bash -c "dokku --quiet config:get $TEST_APP DOKKU_APP_TYPE"
echo "output: $output"
echo "status: $status"

View File

@@ -4,9 +4,11 @@ load test_helper
setup() {
global_setup
create_app
}
teardown() {
destroy_app
global_teardown
}
@@ -24,7 +26,7 @@ teardown() {
assert_output "$help_output"
}
@test "(repo) repo:gc, repo:purge-cache" {
@test "(repo) repo:gc" {
run deploy_app
echo "output: $output"
echo "status: $status"
@@ -34,29 +36,36 @@ teardown() {
echo "output: $output"
echo "status: $status"
assert_success
}
run /bin/bash -c "touch $DOKKU_ROOT/$TEST_APP/cache/derp"
@test "(repo) repo:purge-cache" {
run deploy_app
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "find $DOKKU_ROOT/$TEST_APP/cache -type f | wc -l"
run /bin/bash -c "docker volume ls -q --filter label=com.dokku.app-name=$TEST_APP | wc -l"
echo "count: '$output'"
echo "status: $status"
assert_not_output 0
run /bin/bash -c "dokku repo:purge-cache $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output 1
run /bin/bash -c "find $DOKKU_ROOT/$TEST_APP/cache -type f | wc -l"
echo "count: '$output'"
echo "status: $status"
assert_output 0
run destroy_app
run /bin/bash -c "dokku repo:purge-cache $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "docker volume ls -q --filter label=com.dokku.app-name=$TEST_APP | wc -l"
echo "count: '$output'"
echo "status: $status"
assert_output 0
run /bin/bash -c "find $DOKKU_ROOT/$TEST_APP/cache -type f | wc -l"
echo "count: '$output'"
echo "status: $status"
assert_output 0
}

View File

@@ -535,6 +535,13 @@ add_postdeploy_command() {
echo "${contents}" >"$APP_REPO_DIR/app.json"
}
move_dockerfile_into_place() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
mv "$APP_REPO_DIR/alt.Dockerfile" "$APP_REPO_DIR/Dockerfile"
}
add_requirements_txt() {
local APP="$1"
local APP_REPO_DIR="$2"