From 929b8444e41084541d7f439f4eaa4f7ce6d13100 Mon Sep 17 00:00:00 2001 From: Tasos Maschalidis Date: Wed, 1 Oct 2025 00:27:08 +0300 Subject: [PATCH 001/144] Add helper functions isBareGitRepo and checkoutBareGitRepo --- plugins/buildpacks/functions.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/plugins/buildpacks/functions.go b/plugins/buildpacks/functions.go index f3545233e..0252e239e 100644 --- a/plugins/buildpacks/functions.go +++ b/plugins/buildpacks/functions.go @@ -63,3 +63,31 @@ func validBuildpackURL(buildpack string) (string, error) { return buildpack, fmt.Errorf("Invalid buildpack specified: %v", buildpack) } + +func isBareGitRepo(directory string) bool { + headFile := filepath.Join(directory, "HEAD") + objectsDir := filepath.Join(directory, "objects") + if common.FileExists(headFile) && common.DirectoryExists(objectsDir) { + return true + } + return false +} + +func checkoutBareGitRepo(sourceWorkDir string) (string, error) { + tmpdir, err := os.MkdirTemp("", "bare-git-checkout") + if err != nil { + return "", fmt.Errorf("Unable to create temporary directory: %s", err.Error()) + } + + result, err := common.CallExecCommand(common.ExecCommandInput{ + Command: "git", + Args: []string{"--git-dir=" + sourceWorkDir, "--work-tree=" + tmpdir, "checkout", "-f"}, + }) + + if err != nil { + os.RemoveAll(tmpdir) + return "", fmt.Errorf("Unable to checkout bare git repository: %s", result.StderrContents()) + } + + return tmpdir, nil +} From d440b4614d02c5455a9c940e13f75bf80bf2ec66 Mon Sep 17 00:00:00 2001 From: Tasos Maschalidis Date: Wed, 1 Oct 2025 00:30:09 +0300 Subject: [PATCH 002/144] Define and register 'detect' subcommand --- plugins/buildpacks/Makefile | 2 +- .../buildpacks/src/subcommands/subcommands.go | 5 ++ plugins/buildpacks/subcommands.go | 46 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/plugins/buildpacks/Makefile b/plugins/buildpacks/Makefile index aff09a27a..70ed37a5f 100644 --- a/plugins/buildpacks/Makefile +++ b/plugins/buildpacks/Makefile @@ -1,4 +1,4 @@ -SUBCOMMANDS = subcommands/add subcommands/clear subcommands/list subcommands/remove subcommands/report subcommands/set subcommands/set-property +SUBCOMMANDS = subcommands/add subcommands/clear subcommands/detect subcommands/list subcommands/remove subcommands/report subcommands/set subcommands/set-property TRIGGERS = triggers/buildpack-stack-name triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-delete triggers/post-extract triggers/report BUILD = commands subcommands triggers PLUGIN_NAME = buildpacks diff --git a/plugins/buildpacks/src/subcommands/subcommands.go b/plugins/buildpacks/src/subcommands/subcommands.go index f245238f7..d1c596bcc 100644 --- a/plugins/buildpacks/src/subcommands/subcommands.go +++ b/plugins/buildpacks/src/subcommands/subcommands.go @@ -30,6 +30,11 @@ func main() { args.Parse(os.Args[2:]) appName := args.Arg(0) err = buildpacks.CommandClear(appName) + case "detect": + args := flag.NewFlagSet("buildpacks:detect", flag.ExitOnError) + args.Parse(os.Args[2:]) + appName := args.Arg(0) + err = buildpacks.CommandDetect(appName) case "list": args := flag.NewFlagSet("buildpacks:list", flag.ExitOnError) args.Parse(os.Args[2:]) diff --git a/plugins/buildpacks/subcommands.go b/plugins/buildpacks/subcommands.go index 329ed6b9f..4674eb4e8 100644 --- a/plugins/buildpacks/subcommands.go +++ b/plugins/buildpacks/subcommands.go @@ -3,6 +3,7 @@ package buildpacks import ( "errors" "fmt" + "os" "github.com/dokku/dokku/plugins/common" ) @@ -30,6 +31,51 @@ func CommandClear(appName string) error { return common.PropertyDelete("buildpacks", appName, "buildpacks") } +// CommandDetect implements buildpacks:detect +func CommandDetect(appName string) error { + if err := common.VerifyAppName(appName); err != nil { + return err + } + + buildPath := common.AppRoot(appName) + if !common.DirectoryExists(buildPath) { + return fmt.Errorf("App %s does not exist", appName) + } + + workDir := buildPath + if isBareGitRepo(buildPath) { + checkedOutPath, err := checkoutBareGitRepo(buildPath) + if err != nil { + return err + } + workDir = checkedOutPath + defer func() { + if err := os.RemoveAll(checkedOutPath); err != nil { + common.LogWarn(fmt.Sprintf("Failed to remove temporary directory %s: %v", checkedOutPath, err)) + } + }() + } + + appRoot := workDir + + dockerArgs := []string{ + "run", "--rm", + "-v", fmt.Sprintf("%s:/tmp/app", appRoot), + "gliderlabs/herokuish", "/bin/herokuish", "buildpack", "detect", "/tmp/app", + } + + result, err := common.CallExecCommand(common.ExecCommandInput{ + Command: common.DockerBin(), + Args: dockerArgs, + }) + if err != nil { + return fmt.Errorf("Buildpack detection failed: %s", result.StderrContents()) + } + + common.LogVerbose(fmt.Sprintf("%s", result.StdoutContents())) + return nil +} + // CommandList implements buildpacks:list func CommandList(appName string) (err error) { if err := common.VerifyAppName(appName); err != nil { From 0c79a2c2fc5ac8d6ea956ad51ddf50f59388c3d8 Mon Sep 17 00:00:00 2001 From: Tasos Maschalidis Date: Wed, 1 Oct 2025 00:30:43 +0300 Subject: [PATCH 003/144] Add detect command in buildpacks helpContent --- plugins/buildpacks/src/commands/commands.go | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/buildpacks/src/commands/commands.go b/plugins/buildpacks/src/commands/commands.go index ecbacbc38..c2238a5a4 100644 --- a/plugins/buildpacks/src/commands/commands.go +++ b/plugins/buildpacks/src/commands/commands.go @@ -20,6 +20,7 @@ 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:detect , Detect the buildpack for an 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 From 4b0c8b3d59a4eab41e977fb2b37ba3c61ed6c26d Mon Sep 17 00:00:00 2001 From: Tasos Maschalidis Date: Wed, 1 Oct 2025 00:46:05 +0300 Subject: [PATCH 004/144] Refactor CommandDetect function --- plugins/buildpacks/subcommands.go | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/plugins/buildpacks/subcommands.go b/plugins/buildpacks/subcommands.go index 4674eb4e8..624a42bb1 100644 --- a/plugins/buildpacks/subcommands.go +++ b/plugins/buildpacks/subcommands.go @@ -37,30 +37,23 @@ func CommandDetect(appName string) error { return err } - buildPath := common.AppRoot(appName) - if !common.DirectoryExists(buildPath) { - return fmt.Errorf("App %s does not exist", appName) - } - - workDir := buildPath - if isBareGitRepo(buildPath) { - checkedOutPath, err := checkoutBareGitRepo(buildPath) + workDir := common.AppRoot(appName) + if isBareGitRepo(workDir) { + checkedOutDir, err := checkoutBareGitRepo(workDir) if err != nil { return err } - workDir = checkedOutPath + workDir = checkedOutDir defer func() { - if err := os.RemoveAll(checkedOutPath); err != nil { - common.LogWarn(fmt.Sprintf("Failed to remove temporary directory %s: %v", checkedOutPath, err)) + if err := os.RemoveAll(checkedOutDir); err != nil { + common.LogWarn(fmt.Sprintf("Failed to remove temporary directory %s: %v", checkedOutDir, err)) } }() } - appRoot := workDir - dockerArgs := []string{ "run", "--rm", - "-v", fmt.Sprintf("%s:/tmp/app", appRoot), + "-v", fmt.Sprintf("%s:/tmp/app", workDir), "gliderlabs/herokuish", "/bin/herokuish", "buildpack", "detect", "/tmp/app", } From 66894bc0c5fd7d5b3b2dedc71a03002d01aaf343 Mon Sep 17 00:00:00 2001 From: Tasos Maschalidis Date: Thu, 16 Oct 2025 17:17:29 +0300 Subject: [PATCH 005/144] Remove redundant function isBareGitRepo --- plugins/buildpacks/functions.go | 9 --------- plugins/buildpacks/subcommands.go | 22 ++++++++++------------ 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/plugins/buildpacks/functions.go b/plugins/buildpacks/functions.go index 0252e239e..1845efae8 100644 --- a/plugins/buildpacks/functions.go +++ b/plugins/buildpacks/functions.go @@ -64,15 +64,6 @@ func validBuildpackURL(buildpack string) (string, error) { return buildpack, fmt.Errorf("Invalid buildpack specified: %v", buildpack) } -func isBareGitRepo(directory string) bool { - headFile := filepath.Join(directory, "HEAD") - objectsDir := filepath.Join(directory, "objects") - if common.FileExists(headFile) && common.DirectoryExists(objectsDir) { - return true - } - return false -} - func checkoutBareGitRepo(sourceWorkDir string) (string, error) { tmpdir, err := os.MkdirTemp("", "bare-git-checkout") if err != nil { diff --git a/plugins/buildpacks/subcommands.go b/plugins/buildpacks/subcommands.go index 624a42bb1..64578012d 100644 --- a/plugins/buildpacks/subcommands.go +++ b/plugins/buildpacks/subcommands.go @@ -38,18 +38,16 @@ func CommandDetect(appName string) error { } workDir := common.AppRoot(appName) - if isBareGitRepo(workDir) { - checkedOutDir, err := checkoutBareGitRepo(workDir) - if err != nil { - return err - } - workDir = checkedOutDir - defer func() { - if err := os.RemoveAll(checkedOutDir); err != nil { - common.LogWarn(fmt.Sprintf("Failed to remove temporary directory %s: %v", checkedOutDir, err)) - } - }() - } + checkedOutDir, err := checkoutBareGitRepo(workDir) + if err != nil { + return err + } + workDir = checkedOutDir + defer func() { + if err := os.RemoveAll(checkedOutDir); err != nil { + common.LogWarn(fmt.Sprintf("Failed to remove temporary directory %s: %v", checkedOutDir, err)) + } + }() dockerArgs := []string{ "run", "--rm", From e234401178477e1243b9ef4002b9c11e0671c300 Mon Sep 17 00:00:00 2001 From: Tasos Maschalidis Date: Thu, 16 Oct 2025 17:19:22 +0300 Subject: [PATCH 006/144] Cleanup output log in CommandDetect function --- plugins/buildpacks/subcommands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/buildpacks/subcommands.go b/plugins/buildpacks/subcommands.go index 64578012d..86255e628 100644 --- a/plugins/buildpacks/subcommands.go +++ b/plugins/buildpacks/subcommands.go @@ -63,7 +63,7 @@ func CommandDetect(appName string) error { return fmt.Errorf("Buildpack detection failed: %s", result.StderrContents()) } - common.LogVerbose(fmt.Sprintf("%s", result.StdoutContents())) + common.LogVerbose(result.StdoutContents()) return nil } From 65e0fb9037c998b3556467b2f03174e390ebc546 Mon Sep 17 00:00:00 2001 From: Tasos Maschalidis Date: Thu, 16 Oct 2025 18:33:47 +0300 Subject: [PATCH 007/144] Detect buildpacks for specific branch (fall back to deploy-branch to be consistent with deployment behavior) --- plugins/buildpacks/functions.go | 14 ++++++++++++-- plugins/buildpacks/src/commands/commands.go | 2 +- plugins/buildpacks/src/subcommands/subcommands.go | 6 +++++- plugins/buildpacks/subcommands.go | 4 ++-- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/plugins/buildpacks/functions.go b/plugins/buildpacks/functions.go index 1845efae8..47acbbc78 100644 --- a/plugins/buildpacks/functions.go +++ b/plugins/buildpacks/functions.go @@ -64,15 +64,25 @@ func validBuildpackURL(buildpack string) (string, error) { return buildpack, fmt.Errorf("Invalid buildpack specified: %v", buildpack) } -func checkoutBareGitRepo(sourceWorkDir string) (string, error) { +func checkoutBareGitRepo(sourceWorkDir string, branch string) (string, error) { tmpdir, err := os.MkdirTemp("", "bare-git-checkout") if err != nil { return "", fmt.Errorf("Unable to create temporary directory: %s", err.Error()) } + args := []string{ + "--git-dir=" + sourceWorkDir, + "--work-tree=" + tmpdir, + "checkout", "-f", + } + + if branch != "" { + args = append(args, branch) + } + result, err := common.CallExecCommand(common.ExecCommandInput{ Command: "git", - Args: []string{"--git-dir=" + sourceWorkDir, "--work-tree=" + tmpdir, "checkout", "-f"}, + Args: args, }) if err != nil { diff --git a/plugins/buildpacks/src/commands/commands.go b/plugins/buildpacks/src/commands/commands.go index c2238a5a4..51eb30a15 100644 --- a/plugins/buildpacks/src/commands/commands.go +++ b/plugins/buildpacks/src/commands/commands.go @@ -20,7 +20,7 @@ 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:detect , Detect the buildpack for an app + buildpacks:detect [--branch ], Detect the buildpack for an app (if not specified, the branch flag defaults to the deploy branch) 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 diff --git a/plugins/buildpacks/src/subcommands/subcommands.go b/plugins/buildpacks/src/subcommands/subcommands.go index d1c596bcc..9062e80fa 100644 --- a/plugins/buildpacks/src/subcommands/subcommands.go +++ b/plugins/buildpacks/src/subcommands/subcommands.go @@ -32,9 +32,13 @@ func main() { err = buildpacks.CommandClear(appName) case "detect": args := flag.NewFlagSet("buildpacks:detect", flag.ExitOnError) + branch := args.String("branch", "", "--branch: the branch to detect against (defaults to the deploy branch)") args.Parse(os.Args[2:]) appName := args.Arg(0) - err = buildpacks.CommandDetect(appName) + if *branch == "" { + *branch = common.PropertyGet("git", appName, "deploy-branch") + } + err = buildpacks.CommandDetect(appName, *branch) case "list": args := flag.NewFlagSet("buildpacks:list", flag.ExitOnError) args.Parse(os.Args[2:]) diff --git a/plugins/buildpacks/subcommands.go b/plugins/buildpacks/subcommands.go index 86255e628..079de2091 100644 --- a/plugins/buildpacks/subcommands.go +++ b/plugins/buildpacks/subcommands.go @@ -32,13 +32,13 @@ func CommandClear(appName string) error { } // CommandDetect implements buildpacks:detect -func CommandDetect(appName string) error { +func CommandDetect(appName string, branch string) (err error) { if err := common.VerifyAppName(appName); err != nil { return err } workDir := common.AppRoot(appName) - checkedOutDir, err := checkoutBareGitRepo(workDir) + checkedOutDir, err := checkoutBareGitRepo(workDir, branch) if err != nil { return err } From f40bcc37aa182fc46ad22a96e3d98e60c09a22a8 Mon Sep 17 00:00:00 2001 From: Tasos Maschalidis Date: Thu, 16 Oct 2025 18:35:02 +0300 Subject: [PATCH 008/144] Bump dependency version for herokuish --- contrib/dependencies.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/dependencies.json b/contrib/dependencies.json index 0a9827f4c..9a10d8e84 100644 --- a/contrib/dependencies.json +++ b/contrib/dependencies.json @@ -86,11 +86,11 @@ }, { "name": "herokuish", - "version": "0.11.3", + "version": "0.11.5", "urls": { - "amd64": "https://github.com/gliderlabs/herokuish/releases/download/v0.11.3/herokuish_0.11.3_linux_x86_64.tgz", - "arm64": "https://github.com/gliderlabs/herokuish/releases/download/v0.11.3/herokuish_0.11.3_linux_x86_64.tgz" + "amd64": "https://github.com/gliderlabs/herokuish/releases/download/v0.11.5/herokuish_0.11.5_linux_x86_64.tgz", + "arm64": "https://github.com/gliderlabs/herokuish/releases/download/v0.11.5/herokuish_0.11.5_linux_x86_64.tgz" } } ] -} \ No newline at end of file +} From ca03ea72f567cc7ed8debffb75edca0f838ef747 Mon Sep 17 00:00:00 2001 From: Tasos Maschalidis Date: Fri, 17 Oct 2025 19:14:08 +0300 Subject: [PATCH 009/144] Remove unnecessary re-assignment of workDir variable --- plugins/buildpacks/subcommands.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/buildpacks/subcommands.go b/plugins/buildpacks/subcommands.go index 079de2091..ffabb5eaa 100644 --- a/plugins/buildpacks/subcommands.go +++ b/plugins/buildpacks/subcommands.go @@ -42,7 +42,6 @@ func CommandDetect(appName string, branch string) (err error) { if err != nil { return err } - workDir = checkedOutDir defer func() { if err := os.RemoveAll(checkedOutDir); err != nil { common.LogWarn(fmt.Sprintf("Failed to remove temporary directory %s: %v", checkedOutDir, err)) @@ -51,7 +50,7 @@ func CommandDetect(appName string, branch string) (err error) { dockerArgs := []string{ "run", "--rm", - "-v", fmt.Sprintf("%s:/tmp/app", workDir), + "-v", fmt.Sprintf("%s:/tmp/app", checkedOutDir), "gliderlabs/herokuish", "/bin/herokuish", "buildpack", "detect", "/tmp/app", } From e134c314ea7f8322a5ffebc25201e6a8e2c7f1ab Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 8 Nov 2025 01:02:19 -0500 Subject: [PATCH 010/144] feat: implement config:import command Closes #6846 --- plugins/config/Makefile | 2 +- plugins/config/config.go | 10 +- plugins/config/config_test.go | 83 +++++- plugins/config/environment.go | 39 +++ plugins/config/functions.go | 40 ++- plugins/config/src/commands/commands.go | 1 + plugins/config/src/subcommands/subcommands.go | 16 + plugins/config/subcommands.go | 10 + plugins/ports/functions.go | 4 +- plugins/proxy/proxy.go | 2 +- plugins/proxy/subcommands.go | 2 +- plugins/ps/triggers.go | 4 +- tests/unit/config.bats | 276 ++++++++++++++++++ 13 files changed, 477 insertions(+), 12 deletions(-) diff --git a/plugins/config/Makefile b/plugins/config/Makefile index 0cf396bd8..1f1be8b3c 100644 --- a/plugins/config/Makefile +++ b/plugins/config/Makefile @@ -1,5 +1,5 @@ GOARCH ?= amd64 -SUBCOMMANDS = subcommands/bundle subcommands/clear subcommands/export subcommands/get subcommands/keys subcommands/show subcommands/set subcommands/unset +SUBCOMMANDS = subcommands/bundle subcommands/clear subcommands/export subcommands/get subcommands/import subcommands/keys subcommands/show subcommands/set subcommands/unset TRIGGERS = triggers/config-export triggers/config-get triggers/config-get-global triggers/config-unset triggers/post-app-clone-setup triggers/post-app-rename-setup BUILD = commands config_sub subcommands triggers PLUGIN_NAME = config diff --git a/plugins/config/config.go b/plugins/config/config.go index 90f04039d..18d4d9403 100644 --- a/plugins/config/config.go +++ b/plugins/config/config.go @@ -30,7 +30,7 @@ func GetWithDefault(appName string, key string, defaultValue string) (value stri } // SetMany variables in the environment. If appName is empty the global config is used. If restart is true the app is restarted. -func SetMany(appName string, entries map[string]string, restart bool) (err error) { +func SetMany(appName string, entries map[string]string, replace bool, restart bool) (err error) { global := appName == "" || appName == "--global" env, err := loadAppOrGlobalEnv(appName) if err != nil { @@ -42,6 +42,10 @@ func SetMany(appName string, entries map[string]string, restart bool) (err error return } } + + if replace { + env.Clear() + } for k, v := range entries { env.Set(k, v) keys = append(keys, k) @@ -128,6 +132,10 @@ func UnsetAll(appName string, restart bool) (err error) { } func triggerRestart(appName string) { + if !common.IsDeployed(appName) { + return + } + common.LogInfo1(fmt.Sprintf("Restarting app %s", appName)) _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "release-and-deploy", diff --git a/plugins/config/config_test.go b/plugins/config/config_test.go index e6b9abfce..5c43edbb9 100644 --- a/plugins/config/config_test.go +++ b/plugins/config/config_test.go @@ -128,6 +128,82 @@ func TestConfigUnsetMany(t *testing.T) { Expect(CommandUnset(testAppName+"does-not-exist", keys, false, true)).ToNot(Succeed()) } +func TestConfigImport(t *testing.T) { + RegisterTestingT(t) + Expect(setupTests()).To(Succeed()) + Expect(setupTestApp()).To(Succeed()) + defer teardownTestApp() + + tempFile, err := os.CreateTemp("", "test-config-import-*.env") + Expect(err).To(Succeed()) + defer os.Remove(tempFile.Name()) + content := ` +testKey=TESTING +testKey2=TESTING-'\n'-updated +` + _, err = tempFile.WriteString(content) + Expect(err).To(Succeed()) + tempFile.Close() + + Expect(CommandImport(testAppName, false, false, true, "envfile", tempFile.Name())).To(Succeed()) + expectValue(testAppName, "testKey", "TESTING") + expectValue(testAppName, "testKey2", "TESTING-'\n'-updated") + + env, err := LoadAppEnv(testAppName) + Expect(err).To(Succeed()) + env.Set("testKey", "TESTING-updated2") + env.Set("testKey2", "TESTING-'\n'-updated2") + env.Set("testKey3", "TESTING-'\n'-updated3") + env.Write() + + Expect(CommandImport(testAppName, false, false, true, "envfile", tempFile.Name())).To(Succeed()) + expectValue(testAppName, "testKey", "TESTING-updated2") + expectValue(testAppName, "testKey2", "TESTING-'\n'-updated2") + expectValue(testAppName, "testKey3", "TESTING-'\n'-updated3") + + Expect(CommandImport(testAppName, false, true, true, "envfile", tempFile.Name())).To(Succeed()) + expectValue(testAppName, "testKey", "TESTING-updated") + expectValue(testAppName, "testKey2", "TESTING-'\n'-updated") + expectNoValue(testAppName, "testKey3") +} + +func TestConfigImportJSON(t *testing.T) { + RegisterTestingT(t) + Expect(setupTests()).To(Succeed()) + Expect(setupTestApp()).To(Succeed()) + defer teardownTestApp() + + tempFile, err := os.CreateTemp("", "test-config-import-*.json") + Expect(err).To(Succeed()) + defer os.Remove(tempFile.Name()) + + content := `{"testKey": "TESTING", "testKey2": "TESTING-'\n'-updated"}` + _, err = tempFile.WriteString(content) + Expect(err).To(Succeed()) + tempFile.Close() + + Expect(CommandImport(testAppName, false, false, true, "json", tempFile.Name())).To(Succeed()) + expectValue(testAppName, "testKey", "TESTING") + expectValue(testAppName, "testKey2", "TESTING-'\n'-updated") + + env, err := LoadAppEnv(testAppName) + Expect(err).To(Succeed()) + env.Set("testKey", "TESTING-updated2") + env.Set("testKey2", "TESTING-'\n'-updated2") + env.Set("testKey3", "TESTING-'\n'-updated3") + env.Write() + + Expect(CommandImport(testAppName, false, false, true, "json", tempFile.Name())).To(Succeed()) + expectValue(testAppName, "testKey", "TESTING-updated2") + expectValue(testAppName, "testKey2", "TESTING-'\n'-updated2") + expectValue(testAppName, "testKey3", "TESTING-'\n'-updated3") + + Expect(CommandImport(testAppName, false, true, true, "json", tempFile.Name())).To(Succeed()) + expectValue(testAppName, "testKey", "TESTING-updated") + expectValue(testAppName, "testKey2", "TESTING-'\n'-updated") + expectNoValue(testAppName, "testKey3") +} + func TestEnvironmentLoading(t *testing.T) { RegisterTestingT(t) Expect(setupTests()).To(Succeed()) @@ -161,12 +237,13 @@ func TestInvalidKeys(t *testing.T) { invalidKeys := []string{"0invalidKey", "invalid:key", "invalid=Key", "!invalidKey"} for _, key := range invalidKeys { - Expect(SetMany(testAppName, map[string]string{key: "value"}, false)).NotTo(Succeed()) + Expect(SetMany(testAppName, map[string]string{key: "value"}, false, false)).NotTo(Succeed()) Expect(UnsetMany(testAppName, []string{key}, false)).NotTo(Succeed()) value, ok := Get(testAppName, key) Expect(ok).To(Equal(false)) - value = GetWithDefault(testAppName, key, "default") - Expect(value).To(Equal("default")) + Expect(value).To(Equal("")) + value2 := GetWithDefault(testAppName, key, "default") + Expect(value2).To(Equal("default")) } } diff --git a/plugins/config/environment.go b/plugins/config/environment.go index 877939e5f..6e341dd62 100644 --- a/plugins/config/environment.go +++ b/plugins/config/environment.go @@ -88,6 +88,11 @@ func LoadGlobalEnv() (*Env, error) { return loadFromFile("", getGlobalFile()) } +// Clear clears the environment +func (e *Env) Clear() { + e.env = make(map[string]string) +} + // Filename returns the full path on disk to the file holding the env vars func (e *Env) Filename() string { return e.filename @@ -326,8 +331,17 @@ func prettyPrintEnvEntries(prefix string, entries map[string]string) string { func loadFromFile(name string, filename string) (env *Env, err error) { envMap := make(map[string]string) + if filename == "-" { + envMap, err = godotenv.Parse(os.Stdin) + if err != nil { + return nil, err + } + } if _, err := os.Stat(filename); err == nil { envMap, err = godotenv.Read(filename) + if err != nil { + return nil, err + } } dirty := false @@ -352,6 +366,31 @@ func loadFromFile(name string, filename string) (env *Env, err error) { return } +func loadFromFileJSON(name string, filename string) (env *Env, err error) { + envMap := make(map[string]string) + if filename == "-" { + err = json.NewDecoder(os.Stdin).Decode(&envMap) + if err != nil { + return nil, err + } + } else { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + err = json.NewDecoder(file).Decode(&envMap) + if err != nil { + return nil, err + } + } + + return &Env{ + name: name, + filename: "", + env: envMap, + }, nil +} func getAppFile(appName string) (string, error) { return filepath.Join(common.MustGetEnv("DOKKU_ROOT"), appName, "ENV"), nil } diff --git a/plugins/config/functions.go b/plugins/config/functions.go index 28997d25a..69932ec7c 100644 --- a/plugins/config/functions.go +++ b/plugins/config/functions.go @@ -82,6 +82,44 @@ func SubGet(appName string, keys []string, quoted bool) error { return nil } +// SubImport imports environment variables from a file +func SubImport(appName string, replace bool, noRestart bool, format string, filename string) error { + if filename == "-" { + common.LogInfo1Quiet("Reading config vars from stdin") + } + + if format == "" { + format = "envfile" + } + + var env *Env + var err error + switch format { + case "envfile": + env, err = loadFromFile(appName, filename) + if err != nil { + return err + } + case "json": + env, err = loadFromFileJSON(appName, filename) + if err != nil { + return err + } + default: + return fmt.Errorf("Unknown format: %s", format) + } + + if len(env.Map()) == 0 { + return errors.New("No config vars to set") + } + + for k, v := range env.Map() { + common.LogInfo1Quiet(fmt.Sprintf("Setting config var %s=%s", k, v)) + } + + return SetMany(appName, env.Map(), replace, !noRestart) +} + // SubKeys implements the logic for config:keys without app name validation func SubKeys(appName string, merged bool) error { env := getEnvironment(appName, merged) @@ -115,7 +153,7 @@ func SubSet(appName string, pairs []string, noRestart bool, encoded bool) error updated[key] = value } - return SetMany(appName, updated, !noRestart) + return SetMany(appName, updated, false, !noRestart) } // SubShow implements the logic for config:show without app name validation diff --git a/plugins/config/src/commands/commands.go b/plugins/config/src/commands/commands.go index 78d180de1..ea894fba0 100644 --- a/plugins/config/src/commands/commands.go +++ b/plugins/config/src/commands/commands.go @@ -24,6 +24,7 @@ Additional commands:` config:clear [--no-restart] (|--global), Clears environment variables config:export [--format=FORMAT] [--merged] (|--global), Export a global or app environment config:get [--quoted] (|--global) KEY, Display a global or app-specific config value + config:import [--no-restart] [--replace] (|--global) [FILE|-], Import environment from file config:keys [--merged] (|--global), Show keys set in environment config:show [--merged] (|--global), Show keys set in environment config:set [--encoded] [--no-restart] (|--global) KEY1=VALUE1 [KEY2=VALUE2 ...], Set one or more config vars diff --git a/plugins/config/src/subcommands/subcommands.go b/plugins/config/src/subcommands/subcommands.go index 5fe5bc834..2590c98ed 100644 --- a/plugins/config/src/subcommands/subcommands.go +++ b/plugins/config/src/subcommands/subcommands.go @@ -65,6 +65,22 @@ func main() { } keys := getKeys(args.Args(), *global) err = config.CommandGet(appName, keys, *global, *quoted) + case "import": + args := flag.NewFlagSet("config:import", flag.ExitOnError) + global := args.Bool("global", false, "--global: use the global environment") + noRestart := args.Bool("no-restart", false, "--no-restart: no restart") + replace := args.Bool("replace", false, "--replace: replace existing config vars") + format := args.String("format", "envfile", "--format: [ envfile | json ] which format to export as)") + + args.Parse(os.Args[2:]) + var filename string + if !*global { + appName = args.Arg(0) + filename = args.Arg(1) + } else { + filename = args.Arg(0) + } + err = config.CommandImport(appName, *global, *replace, *noRestart, *format, filename) case "keys": args := flag.NewFlagSet("config:keys", flag.ExitOnError) global := args.Bool("global", false, "--global: use the global environment") diff --git a/plugins/config/subcommands.go b/plugins/config/subcommands.go index fe7e252d6..9b6a45be3 100644 --- a/plugins/config/subcommands.go +++ b/plugins/config/subcommands.go @@ -42,6 +42,16 @@ func CommandGet(appName string, keys []string, global bool, quoted bool) error { return SubGet(appName, keys, quoted) } +// CommandImport imports environment variables from a file +func CommandImport(appName string, global bool, replace bool, noRestart bool, format string, filename string) error { + appName, err := getAppNameOrGlobal(appName, global) + if err != nil { + return err + } + + return SubImport(appName, replace, noRestart, format, filename) +} + // CommandKeys shows the keys set for the specified environment func CommandKeys(appName string, global bool, merged bool) error { appName, err := getAppNameOrGlobal(appName, global) diff --git a/plugins/ports/functions.go b/plugins/ports/functions.go index 29ca80b0d..76aeaed3d 100644 --- a/plugins/ports/functions.go +++ b/plugins/ports/functions.go @@ -449,7 +449,7 @@ func setProxyPort(appName string, port int) error { entries := map[string]string{ "DOKKU_PROXY_PORT": fmt.Sprint(port), } - return config.SetMany(appName, entries, false) + return config.SetMany(appName, entries, false, false) }, map[string]string{"DOKKU_QUIET_OUTPUT": "1"}) } @@ -459,7 +459,7 @@ func setProxySSLPort(appName string, port int) error { entries := map[string]string{ "DOKKU_PROXY_SSL_PORT": fmt.Sprint(port), } - return config.SetMany(appName, entries, false) + return config.SetMany(appName, entries, false, false) }, map[string]string{"DOKKU_QUIET_OUTPUT": "1"}) } diff --git a/plugins/proxy/proxy.go b/plugins/proxy/proxy.go index 57a7bfba6..b674f72b0 100644 --- a/plugins/proxy/proxy.go +++ b/plugins/proxy/proxy.go @@ -41,7 +41,7 @@ func Disable(appName string) error { "DOKKU_DISABLE_PROXY": "1", } - if err := config.SetMany(appName, entries, false); err != nil { + if err := config.SetMany(appName, entries, false, false); err != nil { return err } diff --git a/plugins/proxy/subcommands.go b/plugins/proxy/subcommands.go index 96a90535b..ccf7fa9f5 100644 --- a/plugins/proxy/subcommands.go +++ b/plugins/proxy/subcommands.go @@ -106,5 +106,5 @@ func CommandSet(appName string, proxyType string) error { entries := map[string]string{ key: proxyType, } - return config.SetMany(appName, entries, false) + return config.SetMany(appName, entries, false, false) } diff --git a/plugins/ps/triggers.go b/plugins/ps/triggers.go index 0e355933c..aa2282460 100644 --- a/plugins/ps/triggers.go +++ b/plugins/ps/triggers.go @@ -42,7 +42,7 @@ func TriggerCorePostDeploy(appName string) error { } return common.SuppressOutput(func() error { - return config.SetMany(appName, entries, false) + return config.SetMany(appName, entries, false, false) }) } @@ -245,7 +245,7 @@ func TriggerPostStop(appName string) error { } return common.SuppressOutput(func() error { - return config.SetMany(appName, entries, false) + return config.SetMany(appName, entries, false, false) }) } diff --git a/tests/unit/config.bats b/tests/unit/config.bats index d8d8d1808..e66d3e92a 100644 --- a/tests/unit/config.bats +++ b/tests/unit/config.bats @@ -77,6 +77,282 @@ teardown() { assert_output_not_exists } +@test "(config) config:import stdin" { + run /bin/bash -c "dokku config:set $TEST_APP first_key=first_value" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "echo 'second_key=second_value' | dokku config:import $TEST_APP -" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku config:get $TEST_APP first_key" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "first_value" + run /bin/bash -c "dokku config:get $TEST_APP second_key" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "second_value" + + run /bin/bash -c "echo 'test_var=TESTING2' | dokku config:import $TEST_APP -" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku config:get $TEST_APP first_key" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "first_value" + run /bin/bash -c "dokku config:get $TEST_APP second_key" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "second_value" + run /bin/bash -c "dokku config:get $TEST_APP test_var" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "TESTING2" + + run /bin/bash -c "echo 'test_var=TESTING3' | dokku config:import --replace $TEST_APP -" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku config:get $TEST_APP first_key" + echo "output: $output" + echo "status: $status" + assert_failure + run /bin/bash -c "dokku config:get $TEST_APP second_key" + echo "output: $output" + echo "status: $status" + assert_failure + run /bin/bash -c "dokku config:get $TEST_APP test_var" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "TESTING3" +} + +@test "(config) config:import file" { + local TMP_FILE=$(mktemp "/tmp/config-import.XXXXX") + trap 'popd &>/dev/null || true; rm -rf "$TMP"' INT TERM + + run /bin/bash -c "dokku config:set $TEST_APP first_key=first_value" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "echo 'second_key=second_value' > $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku config:import $TEST_APP $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_failure + + run /bin/bash -c "chown dokku:dokku $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku config:import $TEST_APP $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku config:get $TEST_APP first_key" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "first_value" + run /bin/bash -c "dokku config:get $TEST_APP second_key" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "second_value" + run /bin/bash -c "rm -f $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "echo 'test_var=TESTING2' > $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "chown dokku:dokku $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku config:import $TEST_APP $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku config:get $TEST_APP first_key" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "first_value" + run /bin/bash -c "dokku config:get $TEST_APP second_key" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "second_value" + run /bin/bash -c "dokku config:get $TEST_APP test_var" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "TESTING2" + run /bin/bash -c "rm -f $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "echo 'test_var=TESTING3' > $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "chown dokku:dokku $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku config:import --replace $TEST_APP $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku config:get $TEST_APP first_key" + echo "output: $output" + echo "status: $status" + assert_failure + run /bin/bash -c "dokku config:get $TEST_APP second_key" + echo "output: $output" + echo "status: $status" + assert_failure + run /bin/bash -c "dokku config:get $TEST_APP test_var" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "TESTING3" + run /bin/bash -c "rm -f $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success +} + + +@test "(config) config:import file json" { + local TMP_FILE=$(mktemp "/tmp/config-import.XXXXX") + trap 'popd &>/dev/null || true; rm -rf "$TMP"' INT TERM + + run /bin/bash -c "dokku config:set $TEST_APP first_key=first_value" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "echo '{\"second_key\": \"second_value\"}' > $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku config:import --format json $TEST_APP $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_failure + + run /bin/bash -c "chown dokku:dokku $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku config:import --format json $TEST_APP $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku config:get $TEST_APP first_key" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "first_value" + run /bin/bash -c "dokku config:get $TEST_APP second_key" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "second_value" + run /bin/bash -c "rm -f $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "echo '{\"test_var\": \"TESTING2\"}' > $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "chown dokku:dokku $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku config:import --format json $TEST_APP $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku config:get $TEST_APP first_key" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "first_value" + run /bin/bash -c "dokku config:get $TEST_APP second_key" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "second_value" + run /bin/bash -c "dokku config:get $TEST_APP test_var" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "TESTING2" + run /bin/bash -c "rm -f $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "echo '{\"test_var\": \"TESTING3\"}' > $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "chown dokku:dokku $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku config:import --replace --format json $TEST_APP $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success + run /bin/bash -c "dokku config:get $TEST_APP first_key" + echo "output: $output" + echo "status: $status" + assert_failure + run /bin/bash -c "dokku config:get $TEST_APP second_key" + echo "output: $output" + echo "status: $status" + assert_failure + run /bin/bash -c "dokku config:get $TEST_APP test_var" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "TESTING3" + run /bin/bash -c "rm -f $TMP_FILE" + echo "output: $output" + echo "status: $status" + assert_success +} + @test "(config) config:set/get" { run ssh "dokku@$DOKKU_DOMAIN" config:set $TEST_APP test_var1=true test_var2=\"hello world\" test_var3='double\"quotes' echo "output: $output" From 770d150c7c526b24aadc23b4a07fae9e07281ac8 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 8 Nov 2025 01:17:50 -0500 Subject: [PATCH 011/144] fix: correct lint issue --- tests/unit/config.bats | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/config.bats b/tests/unit/config.bats index e66d3e92a..1825feb12 100644 --- a/tests/unit/config.bats +++ b/tests/unit/config.bats @@ -245,7 +245,6 @@ teardown() { assert_success } - @test "(config) config:import file json" { local TMP_FILE=$(mktemp "/tmp/config-import.XXXXX") trap 'popd &>/dev/null || true; rm -rf "$TMP"' INT TERM From 97f2dc81c715e03be0b8f3aa3d612204e568f379 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 8 Nov 2025 01:22:31 -0500 Subject: [PATCH 012/144] fix: simplify tests --- plugins/config/config_test.go | 46 +++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/plugins/config/config_test.go b/plugins/config/config_test.go index 5c43edbb9..9eb0d0358 100644 --- a/plugins/config/config_test.go +++ b/plugins/config/config_test.go @@ -138,32 +138,32 @@ func TestConfigImport(t *testing.T) { Expect(err).To(Succeed()) defer os.Remove(tempFile.Name()) content := ` -testKey=TESTING -testKey2=TESTING-'\n'-updated +testKey=TESTING-updated1 +testKey2=TESTING-updated2 ` _, err = tempFile.WriteString(content) Expect(err).To(Succeed()) tempFile.Close() Expect(CommandImport(testAppName, false, false, true, "envfile", tempFile.Name())).To(Succeed()) - expectValue(testAppName, "testKey", "TESTING") - expectValue(testAppName, "testKey2", "TESTING-'\n'-updated") + expectValue(testAppName, "testKey", "TESTING-updated1") + expectValue(testAppName, "testKey2", "TESTING-updated2") env, err := LoadAppEnv(testAppName) Expect(err).To(Succeed()) - env.Set("testKey", "TESTING-updated2") - env.Set("testKey2", "TESTING-'\n'-updated2") - env.Set("testKey3", "TESTING-'\n'-updated3") + env.Set("testKey", "TESTING-original1") + env.Set("testKey2", "TESTING-original2") + env.Set("testKey3", "TESTING-original3") env.Write() Expect(CommandImport(testAppName, false, false, true, "envfile", tempFile.Name())).To(Succeed()) - expectValue(testAppName, "testKey", "TESTING-updated2") - expectValue(testAppName, "testKey2", "TESTING-'\n'-updated2") - expectValue(testAppName, "testKey3", "TESTING-'\n'-updated3") + expectValue(testAppName, "testKey", "TESTING-updated1") + expectValue(testAppName, "testKey2", "TESTING-updated2") + expectValue(testAppName, "testKey3", "TESTING-original3") Expect(CommandImport(testAppName, false, true, true, "envfile", tempFile.Name())).To(Succeed()) - expectValue(testAppName, "testKey", "TESTING-updated") - expectValue(testAppName, "testKey2", "TESTING-'\n'-updated") + expectValue(testAppName, "testKey", "TESTING-updated1") + expectValue(testAppName, "testKey2", "TESTING-updated2") expectNoValue(testAppName, "testKey3") } @@ -177,30 +177,30 @@ func TestConfigImportJSON(t *testing.T) { Expect(err).To(Succeed()) defer os.Remove(tempFile.Name()) - content := `{"testKey": "TESTING", "testKey2": "TESTING-'\n'-updated"}` + content := `{"testKey": "TESTING-updated1", "testKey2": "TESTING-updated2"}` _, err = tempFile.WriteString(content) Expect(err).To(Succeed()) tempFile.Close() Expect(CommandImport(testAppName, false, false, true, "json", tempFile.Name())).To(Succeed()) - expectValue(testAppName, "testKey", "TESTING") - expectValue(testAppName, "testKey2", "TESTING-'\n'-updated") + expectValue(testAppName, "testKey", "TESTING-updated1") + expectValue(testAppName, "testKey2", "TESTING-updated2") env, err := LoadAppEnv(testAppName) Expect(err).To(Succeed()) - env.Set("testKey", "TESTING-updated2") - env.Set("testKey2", "TESTING-'\n'-updated2") - env.Set("testKey3", "TESTING-'\n'-updated3") + env.Set("testKey", "TESTING-original1") + env.Set("testKey2", "TESTING-original2") + env.Set("testKey3", "TESTING-original3") env.Write() Expect(CommandImport(testAppName, false, false, true, "json", tempFile.Name())).To(Succeed()) - expectValue(testAppName, "testKey", "TESTING-updated2") - expectValue(testAppName, "testKey2", "TESTING-'\n'-updated2") - expectValue(testAppName, "testKey3", "TESTING-'\n'-updated3") + expectValue(testAppName, "testKey", "TESTING-updated1") + expectValue(testAppName, "testKey2", "TESTING-updated2") + expectValue(testAppName, "testKey3", "TESTING-original3") Expect(CommandImport(testAppName, false, true, true, "json", tempFile.Name())).To(Succeed()) - expectValue(testAppName, "testKey", "TESTING-updated") - expectValue(testAppName, "testKey2", "TESTING-'\n'-updated") + expectValue(testAppName, "testKey", "TESTING-updated1") + expectValue(testAppName, "testKey2", "TESTING-updated2") expectNoValue(testAppName, "testKey3") } From cba345025c6254e75fb45947989ac511309da085 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 8 Sep 2025 04:28:05 -0400 Subject: [PATCH 013/144] feat: add a Railpack builder to Dokku Closes #7615 --- Dockerfile | 5 + docker/etc/sudoers.d/dokku-docker | 2 +- docker/usr/local/bin/railpack | 20 +++ docs/appendices/file-formats/railpack-json.md | 3 + docs/deployment/builders/railpack.md | 163 ++++++++++++++++++ docs/template.html | 1 + plugins/builder-railpack/builder-build | 67 +++++++ plugins/builder-railpack/builder-detect | 17 ++ plugins/builder-railpack/builder-release | 30 ++++ plugins/builder-railpack/commands | 15 ++ plugins/builder-railpack/core-post-extract | 30 ++++ .../dockerfiles/builder-build.Dockerfile | 7 + .../dockerfiles/builder-release.Dockerfile | 2 + plugins/builder-railpack/help-functions | 33 ++++ plugins/builder-railpack/install | 16 ++ plugins/builder-railpack/internal-functions | 116 +++++++++++++ plugins/builder-railpack/plugin.toml | 4 + plugins/builder-railpack/post-app-clone-setup | 14 ++ .../builder-railpack/post-app-rename-setup | 15 ++ plugins/builder-railpack/post-delete | 15 ++ plugins/builder-railpack/report | 6 + plugins/builder-railpack/subcommands/default | 6 + plugins/builder-railpack/subcommands/report | 6 + plugins/builder-railpack/subcommands/set | 30 ++++ plugins/docker-options/docker-args-deploy | 2 +- tests/unit/builder-railpack.bats | 52 ++++++ tests/unit/report.bats | 1 + tests/unit/test_helper.bash | 6 + 28 files changed, 682 insertions(+), 2 deletions(-) create mode 100644 docker/usr/local/bin/railpack create mode 100644 docs/appendices/file-formats/railpack-json.md create mode 100644 docs/deployment/builders/railpack.md create mode 100755 plugins/builder-railpack/builder-build create mode 100755 plugins/builder-railpack/builder-detect create mode 100755 plugins/builder-railpack/builder-release create mode 100755 plugins/builder-railpack/commands create mode 100755 plugins/builder-railpack/core-post-extract create mode 100644 plugins/builder-railpack/dockerfiles/builder-build.Dockerfile create mode 100644 plugins/builder-railpack/dockerfiles/builder-release.Dockerfile create mode 100755 plugins/builder-railpack/help-functions create mode 100755 plugins/builder-railpack/install create mode 100755 plugins/builder-railpack/internal-functions create mode 100644 plugins/builder-railpack/plugin.toml create mode 100755 plugins/builder-railpack/post-app-clone-setup create mode 100755 plugins/builder-railpack/post-app-rename-setup create mode 100755 plugins/builder-railpack/post-delete create mode 100755 plugins/builder-railpack/report create mode 100755 plugins/builder-railpack/subcommands/default create mode 100755 plugins/builder-railpack/subcommands/report create mode 100755 plugins/builder-railpack/subcommands/set create mode 100644 tests/unit/builder-railpack.bats diff --git a/Dockerfile b/Dockerfile index f598c5230..f0b5b79a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,11 @@ RUN mkdir -p /etc/apt/keyrings \ && NIXPACKS_BIN_DIR=/usr/bin BIN_DIR=/usr/bin /tmp/nixpacks.bash \ && test -x /usr/bin/nixpacks \ && rm -rf /tmp/nixpacks.bash \ + && curl -o /tmp/railpack.bash -sSL https://railpack.com/install.sh \ + && chmod +x /tmp/railpack.bash \ + && RAILPACK_BIN_DIR=/usr/bin /tmp/railpack.bash \ + && test -x /usr/bin/railpack \ + && rm -rf /tmp/railpack.bash \ && echo "dokku dokku/hostname string $DOKKU_HOSTNAME" | debconf-set-selections \ && echo "dokku dokku/skip_key_file boolean $DOKKU_SKIP_KEY_FILE" | debconf-set-selections \ && echo "dokku dokku/vhost_enable boolean $DOKKU_VHOST_ENABLE" | debconf-set-selections \ diff --git a/docker/etc/sudoers.d/dokku-docker b/docker/etc/sudoers.d/dokku-docker index bebd88538..f3dc76faa 100644 --- a/docker/etc/sudoers.d/dokku-docker +++ b/docker/etc/sudoers.d/dokku-docker @@ -1 +1 @@ -dokku ALL=NOPASSWD:SETENV:/usr/bin/docker,/usr/bin/docker-container-healthchecker,/usr/bin/docker-image-labeler,/usr/bin/lambda-builder,/usr/bin/nixpacks,/usr/bin/pack +dokku ALL=NOPASSWD:SETENV:/usr/bin/docker,/usr/bin/docker-container-healthchecker,/usr/bin/docker-image-labeler,/usr/bin/lambda-builder,/usr/bin/nixpacks,/usr/bin/railpack,/usr/bin/pack diff --git a/docker/usr/local/bin/railpack b/docker/usr/local/bin/railpack new file mode 100644 index 000000000..35bee71d2 --- /dev/null +++ b/docker/usr/local/bin/railpack @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $TRACE ]] && set -x + +main() { + declare desc="re-runs railpack commands as sudo" + local RAILPACK_BIN="" + if [[ -x "/usr/bin/railpack" ]]; then + RAILPACK_BIN="/usr/bin/railpack" + fi + + if [[ -z "$RAILPACK_BIN" ]]; then + echo "! No railpack binary found" 1>&2 + exit 1 + fi + + sudo -E "$RAILPACK_BIN" "$@" +} + +main "$@" diff --git a/docs/appendices/file-formats/railpack-json.md b/docs/appendices/file-formats/railpack-json.md new file mode 100644 index 000000000..d6e390fb4 --- /dev/null +++ b/docs/appendices/file-formats/railpack-json.md @@ -0,0 +1,3 @@ +# railpack.json + +The `railpack.json` file is used to configure an application when built with the `railpack` builder. Please refer to the [railpack.json documentation](https://railpack.com/config/file) for more information. diff --git a/docs/deployment/builders/railpack.md b/docs/deployment/builders/railpack.md new file mode 100644 index 000000000..3483c7d72 --- /dev/null +++ b/docs/deployment/builders/railpack.md @@ -0,0 +1,163 @@ +# Railpack + +> [!IMPORTANT] +> New as of 0.32.0 + +The `railpack` builder builds apps via [Railpack](https://railpack.com/), a buildpack alternative. + +## Usage + +### Requirements + +The `railpack` cli tool is not included by default with Dokku or as a dependency. It must also be installed as shown on [this page](https://railpack.com/installation). + +Builds will proceed with the `railpack` cli for the app from then on. + +### Detection + +This builder will be auto-detected in the following case: + +- A `railpack.json` exists in the root of the app repository. + +The builder may also be selected via the `builder:set` command + +```shell +dokku builder:set node-js-app selected railpack +``` + +### Supported languages + +See the [upstream railpack documentation](https://railpack.com/) for further information on what languages and frameworks are supported. + +### Build-time configuration variables + +For security reasons - and as per [Docker recommendations](https://github.com/docker/docker/issues/13490) - railpack-based deploys have variables available only during runtime. + +For users that require customization in the `build` phase, you may use build arguments via the [docker-options plugin](/docs/advanced-usage/docker-options.md). All environment variables set by the `config` plugin are automatically exported within the railpack build environment, and thus `--env` only requires setting a key without a value. + +```shell +dokku docker-options:add node-js-app build '--env NODE_ENV' +``` + +Alternatively, a full value may be provided in the form of `--env KEY=VALUE`: + +```shell +dokku docker-options:add node-js-app build '--env NODE_ENV=production' +``` + +### Changing the `railpack.json` location + +The `railpack.json` is expected to be found in a specific directory, depending on the deploy approach: + +- The `WORKDIR` of the Docker image for deploys resulting from `git:from-image` and `git:load-image` commands. +- The root of the source code tree for all other deploys (git push, `git:from-archive`, `git:sync`). + +Sometimes it may be desirable to set a different path for a given app, e.g. when deploying from a monorepo. This can be done via the `railpackjson-path` property: + +```shell +dokku builder-railpack:set node-js-app railpackjson-path .dokku/railpack.json +``` + +The value is the path to the desired file *relative* to the base search directory, and will never be treated as absolute paths in any context. If that file does not exist within the repository, the build will fail. + +The default value may be set by passing an empty value for the option: + +```shell +dokku builder-railpack:set node-js-app railpackjson-path +``` + +The `railpackjson-path` property can also be set globally. The global default is `railpack.json`, and the global value is used when no app-specific value is set. + +```shell +dokku builder-railpack:set --global railpackjson-path railpack2.json +``` + +The default value may be set by passing an empty value for the option. + +```shell +dokku builder-railpack:set --global railpackjson-path +``` + +### Disabling cache + +Cache is enabled by default, but can be disabled by setting the `no-cache` property to `true`: + +```shell +dokku builder-railpack:set node-js-app no-cache true +``` + +The default value may be set by passing an empty value for the option: + +```shell +dokku builder-railpack:set node-js-app no-cache +``` + +The `no-cache` property can also be set globally. The global default is `false`, and the global value is used when no app-specific value is set. + +```shell +dokku builder-railpack:set --global no-cache true +``` + +The default value may be set by passing an empty value for the option. + +```shell +dokku builder-railpack:set --global no-cache +``` + +### Displaying builder-railpack reports for an app + +You can get a report about the app's storage status using the `builder-railpack:report` command: + +```shell +dokku builder-railpack:report +``` + +``` +=====> node-js-app builder-railpack information + Builder-railpack computed railpackjson path: railpack2.json + Builder-railpack global railpackjson path: railpack.json + Builder-railpack railpackjson path: railpack2.json + Builder-railpack computed no cache: true + Builder-railpack global no cache: false + Builder-railpack no cache: true +=====> python-sample builder-railpack information + Builder-railpack computed railpackjson path: railpack.json + Builder-railpack global railpackjson path: railpack.json + Builder-railpack railpackjson path: + Builder-railpack computed no cache: false + Builder-railpack global no cache: false + Builder-railpack no cache: +=====> ruby-sample builder-railpack information + Builder-railpack computed railpackjson path: railpack.json + Builder-railpack global railpackjson path: railpack.json + Builder-railpack railpackjson path: + Builder-railpack computed no cache: false + Builder-railpack global no cache: false + Builder-railpack no cache: +``` + +You can run the command for a specific app also. + +```shell +dokku builder-railpack:report node-js-app +``` + +``` +=====> node-js-app builder-railpack information + Builder-railpack computed railpackjson path: railpack2.json + Builder-railpack global railpackjson path: railpack.json + Builder-railpack railpackjson path: railpack2.json + Builder-railpack computed no cache: true + Builder-railpack global no cache: false + Builder-railpack no cache: true +``` + +You can pass flags which will output only the value of the specific information you want. For example: + +```shell +dokku builder-railpack:report node-js-app --builder-railpack-no-cache +``` + +``` +true +``` diff --git a/docs/template.html b/docs/template.html index d81802c40..9e26b3759 100644 --- a/docs/template.html +++ b/docs/template.html @@ -150,6 +150,7 @@ Lambda Builder Nixpacks Builder Null Builder + Railpack Builder Deployment Methods Git Deployment diff --git a/plugins/builder-railpack/builder-build b/plugins/builder-railpack/builder-build new file mode 100755 index 000000000..c1664fcac --- /dev/null +++ b/plugins/builder-railpack/builder-build @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/builder-railpack/internal-functions" +source "$PLUGIN_AVAILABLE_PATH/config/functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-builder-railpack-builder-build() { + declare desc="builder-railpack builder-build plugin trigger" + declare trigger="builder-build" + declare BUILDER_TYPE="$1" APP="$2" SOURCECODE_WORK_DIR="$3" + + if [[ "$BUILDER_TYPE" != "railpack" ]]; then + return + fi + + dokku_log_info1 "Building $APP from railpack" + + if ! command -v "railpack" &>/dev/null; then + dokku_log_fail "Missing railpack, install it" + fi + + local IMAGE=$(get_app_image_name "$APP") + local DOCKER_BUILD_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=railpack" "--label=com.dokku.app-name=$APP") + + pushd "$SOURCECODE_WORK_DIR" &>/dev/null + + plugn trigger pre-build "$BUILDER_TYPE" "$APP" "$SOURCECODE_WORK_DIR" + + no_cache="$(fn-builder-railpack-computed-no-cache "$APP")" + RAILPACK_ARGS="" + if [[ "$no_cache" == "true" ]]; then + RAILPACK_ARGS="$RAILPACK_ARGS --no-cache" + fi + + local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$BUILDER_TYPE") + DOCKER_ARGS+=$(: | plugn trigger docker-args-process-build "$APP" "$BUILDER_TYPE") + + # strip --link, --volume and -v args from DOCKER_ARGS + local DOCKER_ARGS=$(sed -e "s/^--link=[[:graph:]]\+[[:blank:]]\?//g" -e "s/^--link[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" -e "s/^--volume=[[:graph:]]\+[[:blank:]]\?//g" -e "s/^--volume[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" -e "s/^-v[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" <<<"$DOCKER_ARGS") + declare -a ARG_ARRAY + eval "ARG_ARRAY=($DOCKER_ARGS)" + + eval "$(config_export app "$APP" --merged)" + + if [[ -f "$SOURCECODE_WORK_DIR/Procfile" ]]; then + if procfile-util exists --process-type release; then + procfile-util delete --process-type release + fi + fi + + # shellcheck disable=SC2086 + if ! railpack build "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS "${ARG_ARRAY[@]}" $RAILPACK_ARGS --name "$IMAGE" "$SOURCECODE_WORK_DIR"; then + dokku_log_warn "Failure building image" + return 1 + fi + + if ! suppress_output "$DOCKER_BIN" image build -f "$PLUGIN_AVAILABLE_PATH/builder-railpack/dockerfiles/builder-build.Dockerfile" --build-arg APP_IMAGE="$IMAGE" -t "$IMAGE" "$SOURCECODE_WORK_DIR"; then + dokku_log_warn "Failure injecting docker labels and custom entrypoint on image" + return 1 + fi + + plugn trigger post-build "$BUILDER_TYPE" "$APP" "$SOURCECODE_WORK_DIR" + popd &>/dev/null || pushd "/tmp" >/dev/null +} + +trigger-builder-railpack-builder-build "$@" diff --git a/plugins/builder-railpack/builder-detect b/plugins/builder-railpack/builder-detect new file mode 100755 index 000000000..1ba0b89ba --- /dev/null +++ b/plugins/builder-railpack/builder-detect @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/config/functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-builder-railpack-builder-detect() { + declare desc="builder-railpack builder-detect plugin trigger" + declare trigger="builder-detect" + declare APP="$1" SOURCECODE_WORK_DIR="$2" + + if [[ -f "$SOURCECODE_WORK_DIR/railpack.json" ]]; then + echo "railpack" + return + fi +} + +trigger-builder-railpack-builder-detect "$@" diff --git a/plugins/builder-railpack/builder-release b/plugins/builder-railpack/builder-release new file mode 100755 index 000000000..7c3650421 --- /dev/null +++ b/plugins/builder-railpack/builder-release @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-builder-railpack-builder-release() { + declare desc="builder-railpack builder-release plugin trigger" + declare trigger="builder-release" + declare BUILDER_TYPE="$1" APP="$2" IMAGE_TAG="$3" + + if [[ "$BUILDER_TYPE" != "railpack" ]]; then + return + fi + + local IMAGE=$(get_app_image_name "$APP" "$IMAGE_TAG") + plugn trigger pre-release-builder "$BUILDER_TYPE" "$APP" "$IMAGE" + + TMP_WORK_DIR="$(mktemp -d "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX")" + trap "rm -rf '$TMP_WORK_DIR' >/dev/null" RETURN INT TERM EXIT + + local DOCKER_BUILD_LABEL_ARGS="--label=org.label-schema.schema-version=1.0 --label=org.label-schema.vendor=dokku --label=com.dokku.app-name=$APP --label=com.dokku.image-stage=release --label=dokku" + if ! suppress_output "$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -f "$PLUGIN_AVAILABLE_PATH/builder-railpack/dockerfiles/builder-release.Dockerfile" --build-arg APP_IMAGE="$IMAGE" -t "$IMAGE" "$TMP_WORK_DIR"; then + dokku_log_warn "Failure injecting docker labels on image" + return 1 + fi + + plugn trigger post-release-builder "$BUILDER_TYPE" "$APP" "$IMAGE" +} + +trigger-builder-railpack-builder-release "$@" diff --git a/plugins/builder-railpack/commands b/plugins/builder-railpack/commands new file mode 100755 index 000000000..db753d311 --- /dev/null +++ b/plugins/builder-railpack/commands @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_AVAILABLE_PATH/builder-railpack/help-functions" + +case "$1" in + help | builder-railpack:help) + cmd-builder-railpack-help "$@" + ;; + + *) + exit "$DOKKU_NOT_IMPLEMENTED_EXIT" + ;; + +esac diff --git a/plugins/builder-railpack/core-post-extract b/plugins/builder-railpack/core-post-extract new file mode 100755 index 000000000..ad863c076 --- /dev/null +++ b/plugins/builder-railpack/core-post-extract @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/builder-railpack/internal-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-builder-railpack-core-post-extract() { + declare desc="builder-railpack post-extract plugin trigger" + declare trigger="post-extract" + declare APP="$1" SOURCECODE_WORK_DIR="$2" + local NEW_RAILPACK_JSON="$(fn-builder-railpack-computed-railpackjson-path "$APP")" + + pushd "$TMP_WORK_DIR" >/dev/null + + if [[ -z "$NEW_RAILPACK_JSON" ]]; then + return + fi + + if [[ ! -f "$NEW_RAILPACK_JSON" ]]; then + rm -f railpack.json + return + fi + + if [[ "$NEW_RAILPACK_JSON" != "railpack.json" ]]; then + mv "$NEW_RAILPACK_JSON" railpack.json + fi + popd &>/dev/null || pushd "/tmp" >/dev/null +} + +trigger-builder-railpack-core-post-extract "$@" diff --git a/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile b/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile new file mode 100644 index 000000000..57aa80d1c --- /dev/null +++ b/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile @@ -0,0 +1,7 @@ +ARG APP_IMAGE +FROM $APP_IMAGE + +RUN printf '#!/usr/bin/env bash\nexec bash -l -c -- \"$*\"\n' > /usr/local/bin/entrypoint && \ + chmod +x /usr/local/bin/entrypoint + +ENTRYPOINT ["/usr/local/bin/entrypoint"] diff --git a/plugins/builder-railpack/dockerfiles/builder-release.Dockerfile b/plugins/builder-railpack/dockerfiles/builder-release.Dockerfile new file mode 100644 index 000000000..cca256f21 --- /dev/null +++ b/plugins/builder-railpack/dockerfiles/builder-release.Dockerfile @@ -0,0 +1,2 @@ +ARG APP_IMAGE +FROM $APP_IMAGE diff --git a/plugins/builder-railpack/help-functions b/plugins/builder-railpack/help-functions new file mode 100755 index 000000000..ef1811401 --- /dev/null +++ b/plugins/builder-railpack/help-functions @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-builder-railpack-help() { + declare desc="help command" + declare CMD="$1" + local plugin_name="builder-railpack" + local plugin_description="Manage the railpack builder integration for an app" + + if [[ "$CMD" == "${plugin_name}:help" ]]; then + echo -e "Usage: dokku ${plugin_name}[:COMMAND]" + echo '' + echo "$plugin_description" + echo '' + echo 'Additional commands:' + fn-help-content | sort | column -c2 -t -s, + elif [[ $(ps -o command= $PPID) == *"--all"* ]]; then + fn-help-content + else + cat <] [], Displays a builder-railpack report for one or more apps + builder-railpack:set (), Set or clear a builder-railpack property for an app +help_content +} diff --git a/plugins/builder-railpack/install b/plugins/builder-railpack/install new file mode 100755 index 000000000..283188a33 --- /dev/null +++ b/plugins/builder-railpack/install @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-builder-railpack-install() { + declare desc="installs the builder-railpack plugin" + declare trigger="install" + + fn-plugin-property-setup "builder-railpack" + + mkdir -p "${DOKKU_LIB_ROOT}/data/builder-railpack" + chown -R "${DOKKU_SYSTEM_USER}:${DOKKU_SYSTEM_GROUP}" "${DOKKU_LIB_ROOT}/data/builder-railpack" +} + +trigger-builder-railpack-install "$@" diff --git a/plugins/builder-railpack/internal-functions b/plugins/builder-railpack/internal-functions new file mode 100755 index 000000000..a405b2ed0 --- /dev/null +++ b/plugins/builder-railpack/internal-functions @@ -0,0 +1,116 @@ +#!/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 + +cmd-builder-railpack-report() { + declare desc="displays a builder-railpack report for one or more apps" + declare cmd="builder-railpack:report" + [[ "$1" == "$cmd" ]] && shift 1 + declare APP="$1" INFO_FLAG="$2" + + if [[ -n "$APP" ]] && [[ "$APP" == --* ]]; then + INFO_FLAG="$APP" + APP="" + fi + + if [[ -z "$APP" ]] && [[ -z "$INFO_FLAG" ]]; then + INFO_FLAG="true" + fi + + if [[ -z "$APP" ]]; then + for app in $(dokku_apps); do + cmd-builder-railpack-report-single "$app" "$INFO_FLAG" | tee || true + done + else + cmd-builder-railpack-report-single "$APP" "$INFO_FLAG" + fi +} + +cmd-builder-railpack-report-single() { + declare APP="$1" INFO_FLAG="$2" + if [[ "$INFO_FLAG" == "true" ]]; then + INFO_FLAG="" + fi + verify_app_name "$APP" + local flag_map=( + "--builder-railpack-computed-railpackjson-path: $(fn-builder-railpack-computed-railpackjson-path "$APP")" + "--builder-railpack-global-railpackjson-path: $(fn-builder-railpack-global-railpackjson-path "$APP")" + "--builder-railpack-railpackjson-path: $(fn-builder-railpack-railpackjson-path "$APP")" + "--builder-railpack-computed-no-cache: $(fn-builder-railpack-computed-no-cache "$APP")" + "--builder-railpack-global-no-cache: $(fn-builder-railpack-global-no-cache "$APP")" + "--builder-railpack-no-cache: $(fn-builder-railpack-no-cache "$APP")" + ) + + if [[ -z "$INFO_FLAG" ]]; then + dokku_log_info2_quiet "${APP} builder-railpack information" + for flag in "${flag_map[@]}"; do + key="$(echo "${flag#--}" | cut -f1 -d' ' | tr - ' ')" + dokku_log_verbose "$(printf "%-30s %-25s" "${key^}" "${flag#*: }")" + done + else + local match=false + local value_exists=false + for flag in "${flag_map[@]}"; do + valid_flags="${valid_flags} $(echo "$flag" | cut -d':' -f1)" + if [[ "$flag" == "${INFO_FLAG}:"* ]]; then + value=${flag#*: } + size="${#value}" + if [[ "$size" -ne 0 ]]; then + echo "$value" && match=true && value_exists=true + else + match=true + fi + fi + done + [[ "$match" == "true" ]] || dokku_log_fail "Invalid flag passed, valid flags:${valid_flags}" + [[ "$value_exists" == "true" ]] || dokku_log_fail "not deployed" + fi +} + +fn-builder-railpack-computed-railpackjson-path() { + declare APP="$1" + + file="$(fn-builder-railpack-railpackjson-path "$APP")" + if [[ "$file" == "" ]]; then + file="$(fn-builder-railpack-global-railpackjson-path "$APP")" + fi + + echo "$file" +} + +fn-builder-railpack-global-railpackjson-path() { + declare APP="$1" + + fn-plugin-property-get "builder-railpack" "--global" "railpackjson-path" "railpack.json" +} + +fn-builder-railpack-railpackjson-path() { + declare APP="$1" + + fn-plugin-property-get "builder-railpack" "$APP" "railpackjson-path" "" +} + +fn-builder-railpack-computed-no-cache() { + declare APP="$1" + + file="$(fn-builder-railpack-no-cache "$APP")" + if [[ "$file" == "" ]]; then + file="$(fn-builder-railpack-global-no-cache "$APP")" + fi + + echo "$file" +} + +fn-builder-railpack-global-no-cache() { + declare APP="$1" + + fn-plugin-property-get "builder-railpack" "--global" "no-cache" "false" +} + +fn-builder-railpack-no-cache() { + declare APP="$1" + + fn-plugin-property-get "builder-railpack" "$APP" "no-cache" "" +} diff --git a/plugins/builder-railpack/plugin.toml b/plugins/builder-railpack/plugin.toml new file mode 100644 index 000000000..d7eceec7d --- /dev/null +++ b/plugins/builder-railpack/plugin.toml @@ -0,0 +1,4 @@ +[plugin] +description = "dokku core builder-railpack plugin" +version = "0.36.5" +[plugin.config] diff --git a/plugins/builder-railpack/post-app-clone-setup b/plugins/builder-railpack/post-app-clone-setup new file mode 100755 index 000000000..0ff83fc92 --- /dev/null +++ b/plugins/builder-railpack/post-app-clone-setup @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-builder-railpack-post-app-clone-setup() { + declare desc="sets up properties for new app" + declare trigger="post-app-clone-setup" + declare OLD_APP="$1" NEW_APP="$2" + + fn-plugin-property-clone "builder-railpack" "$OLD_APP" "$NEW_APP" +} + +trigger-builder-railpack-post-app-clone-setup "$@" diff --git a/plugins/builder-railpack/post-app-rename-setup b/plugins/builder-railpack/post-app-rename-setup new file mode 100755 index 000000000..10effd4e5 --- /dev/null +++ b/plugins/builder-railpack/post-app-rename-setup @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-builder-railpack-post-app-rename-setup() { + declare desc="updates settings for new app" + declare trigger="post-app-rename-setup" + declare OLD_APP="$1" NEW_APP="$2" + + fn-plugin-property-clone "builder-railpack" "$OLD_APP" "$NEW_APP" + fn-plugin-property-destroy "builder-railpack" "$OLD_APP" +} + +trigger-builder-railpack-post-app-rename-setup "$@" diff --git a/plugins/builder-railpack/post-delete b/plugins/builder-railpack/post-delete new file mode 100755 index 000000000..5879fc046 --- /dev/null +++ b/plugins/builder-railpack/post-delete @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-builder-railpack-post-delete() { + declare desc="destroys the builder-railpack properties for a given app" + declare trigger="post-delete" + declare APP="$1" + + fn-plugin-property-destroy "builder-railpack" "$APP" + rm -rf "${DOKKU_LIB_ROOT}/data/builder-railpack/$APP" +} + +trigger-builder-railpack-post-delete "$@" diff --git a/plugins/builder-railpack/report b/plugins/builder-railpack/report new file mode 100755 index 000000000..6fbd0d855 --- /dev/null +++ b/plugins/builder-railpack/report @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/builder-railpack/internal-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-builder-railpack-report-single "$@" diff --git a/plugins/builder-railpack/subcommands/default b/plugins/builder-railpack/subcommands/default new file mode 100755 index 000000000..cc0e6748a --- /dev/null +++ b/plugins/builder-railpack/subcommands/default @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/builder-railpack/help-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-builder-railpack-help "builder-railpack:help" diff --git a/plugins/builder-railpack/subcommands/report b/plugins/builder-railpack/subcommands/report new file mode 100755 index 000000000..9801d5f29 --- /dev/null +++ b/plugins/builder-railpack/subcommands/report @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/builder-railpack/internal-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-builder-railpack-report "$@" diff --git a/plugins/builder-railpack/subcommands/set b/plugins/builder-railpack/subcommands/set new file mode 100755 index 000000000..6001f0c6f --- /dev/null +++ b/plugins/builder-railpack/subcommands/set @@ -0,0 +1,30 @@ +#!/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 + +cmd-builder-railpack-set() { + declare desc="set or clear a builder-railpack property for an app" + declare cmd="builder-railpack:set" + [[ "$1" == "$cmd" ]] && shift 1 + declare APP="$1" KEY="$2" VALUE="$3" + local VALID_KEYS=("railpackjson-path" "no-cache") + [[ "$APP" == "--global" ]] || verify_app_name "$APP" + + [[ -z "$KEY" ]] && dokku_log_fail "No key specified" + + if ! fn-in-array "$KEY" "${VALID_KEYS[@]}"; then + dokku_log_fail "Invalid key specified, valid keys include: railpackjson-path, no-cache" + fi + + if [[ -n "$VALUE" ]]; then + dokku_log_info2_quiet "Setting ${KEY} to ${VALUE}" + fn-plugin-property-write "builder-railpack" "$APP" "$KEY" "$VALUE" + else + dokku_log_info2_quiet "Unsetting ${KEY}" + fn-plugin-property-delete "builder-railpack" "$APP" "$KEY" + fi +} + +cmd-builder-railpack-set "$@" diff --git a/plugins/docker-options/docker-args-deploy b/plugins/docker-options/docker-args-deploy index fc768a0cf..35ea4bc97 100755 --- a/plugins/docker-options/docker-args-deploy +++ b/plugins/docker-options/docker-args-deploy @@ -47,7 +47,7 @@ trigger-docker-options-docker-args() { *) case "$IMAGE_SOURCE_TYPE" in - dockerfile | nixpacks) + dockerfile | nixpacks | railpack) case "$line" in --link* | -v* | --volume*) continue diff --git a/tests/unit/builder-railpack.bats b/tests/unit/builder-railpack.bats new file mode 100644 index 000000000..017123f0c --- /dev/null +++ b/tests/unit/builder-railpack.bats @@ -0,0 +1,52 @@ +#!/usr/bin/env bats + +load test_helper + +setup_file() { + install_railpack +} + +setup() { + create_app +} + +teardown() { + destroy_app +} + +@test "(builder-railpack:set)" { + run /bin/bash -c "dokku config:set $TEST_APP SECRET_KEY=fjdkslafjdk" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku builder:set $TEST_APP selected railpack" + echo "output: $output" + echo "status: $status" + assert_success + + run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP inject_requirements_txt + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains 'load build definition from Dockerfile' + assert_output_contains "SECRET_KEY: fjdkslafjdk" + + run /bin/bash -c "dokku builder-railpack:set $TEST_APP railpackjson-path nonexistent.json" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku ps:rebuild $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains 'load build definition from Dockerfile' +} + +inject_requirements_txt() { + local APP="$1" + local APP_REPO_DIR="$2" + [[ -z "$APP" ]] && local APP="$TEST_APP" + echo "flask" >>"$APP_REPO_DIR/requirements.txt" +} diff --git a/tests/unit/report.bats b/tests/unit/report.bats index a5f7271b6..719af8c80 100644 --- a/tests/unit/report.bats +++ b/tests/unit/report.bats @@ -103,6 +103,7 @@ teardown() { builder-herokuish builder-lambda builder-nixpacks + builder-railpack builder-pack buildpacks caddy diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index 34ce53bcb..c53880f24 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -660,6 +660,12 @@ install_nixpacks() { fi } +install_railpack() { + if ! command -v "railpack" &>/dev/null; then + curl -sSL https://railpack.com/install.sh | FORCE=1 bash + fi +} + install_k3s() { run /bin/bash -c "dokku proxy:set --global k3s" echo "output: $output" From ee467e679357f0c6474d41e15d2fccd062bf92df Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 14 Sep 2025 21:11:59 -0400 Subject: [PATCH 014/144] fix: inject labels after the image is built by railpack Railpack doesn't support the --label flag. --- plugins/builder-railpack/builder-build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/builder-railpack/builder-build b/plugins/builder-railpack/builder-build index c1664fcac..52bffedd4 100755 --- a/plugins/builder-railpack/builder-build +++ b/plugins/builder-railpack/builder-build @@ -50,12 +50,12 @@ trigger-builder-railpack-builder-build() { fi # shellcheck disable=SC2086 - if ! railpack build "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS "${ARG_ARRAY[@]}" $RAILPACK_ARGS --name "$IMAGE" "$SOURCECODE_WORK_DIR"; then + if ! railpack build "${ARG_ARRAY[@]}" $RAILPACK_ARGS --name "$IMAGE" "$SOURCECODE_WORK_DIR"; then dokku_log_warn "Failure building image" return 1 fi - if ! suppress_output "$DOCKER_BIN" image build -f "$PLUGIN_AVAILABLE_PATH/builder-railpack/dockerfiles/builder-build.Dockerfile" --build-arg APP_IMAGE="$IMAGE" -t "$IMAGE" "$SOURCECODE_WORK_DIR"; then + if ! suppress_output "$DOCKER_BIN" image build -f "$PLUGIN_AVAILABLE_PATH/builder-railpack/dockerfiles/builder-build.Dockerfile" --build-arg APP_IMAGE="$IMAGE" "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -t "$IMAGE" "$SOURCECODE_WORK_DIR"; then dokku_log_warn "Failure injecting docker labels and custom entrypoint on image" return 1 fi From 9f334592661eb269b2b43ebbf8d0aac848abe3ca Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 14 Sep 2025 21:43:33 -0400 Subject: [PATCH 015/144] fix: set BIN_DIR --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f0b5b79a8..dead75810 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,7 @@ RUN mkdir -p /etc/apt/keyrings \ && rm -rf /tmp/nixpacks.bash \ && curl -o /tmp/railpack.bash -sSL https://railpack.com/install.sh \ && chmod +x /tmp/railpack.bash \ - && RAILPACK_BIN_DIR=/usr/bin /tmp/railpack.bash \ + && RAILPACK_BIN_DIR=/usr/bin BIN_DIR=/usr/bin /tmp/railpack.bash \ && test -x /usr/bin/railpack \ && rm -rf /tmp/railpack.bash \ && echo "dokku dokku/hostname string $DOKKU_HOSTNAME" | debconf-set-selections \ From 6b27f424db4784643816727cbb68e6c1b6c57b00 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 14 Sep 2025 21:43:39 -0400 Subject: [PATCH 016/144] fix: remove extra space --- plugins/builder-railpack/builder-build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/builder-railpack/builder-build b/plugins/builder-railpack/builder-build index 52bffedd4..b64dce3a6 100755 --- a/plugins/builder-railpack/builder-build +++ b/plugins/builder-railpack/builder-build @@ -55,7 +55,7 @@ trigger-builder-railpack-builder-build() { return 1 fi - if ! suppress_output "$DOCKER_BIN" image build -f "$PLUGIN_AVAILABLE_PATH/builder-railpack/dockerfiles/builder-build.Dockerfile" --build-arg APP_IMAGE="$IMAGE" "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -t "$IMAGE" "$SOURCECODE_WORK_DIR"; then + if ! suppress_output "$DOCKER_BIN" image build -f "$PLUGIN_AVAILABLE_PATH/builder-railpack/dockerfiles/builder-build.Dockerfile" --build-arg APP_IMAGE="$IMAGE" "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -t "$IMAGE" "$SOURCECODE_WORK_DIR"; then dokku_log_warn "Failure injecting docker labels and custom entrypoint on image" return 1 fi From 300a276ed0d43aa1e72059b2c156c4ecd221c718 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 14 Sep 2025 21:44:19 -0400 Subject: [PATCH 017/144] chore: bump version in plugin.toml --- plugins/builder-railpack/plugin.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/builder-railpack/plugin.toml b/plugins/builder-railpack/plugin.toml index d7eceec7d..6fa72020b 100644 --- a/plugins/builder-railpack/plugin.toml +++ b/plugins/builder-railpack/plugin.toml @@ -1,4 +1,4 @@ [plugin] description = "dokku core builder-railpack plugin" -version = "0.36.5" +version = "0.36.7" [plugin.config] From 084097626557502de98775403741f057ebb91eea Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 15 Sep 2025 00:21:50 -0400 Subject: [PATCH 018/144] fix: add references to BUILDKIT_HOST for setup and fix tests --- docs/deployment/builders/railpack.md | 13 +++++++++++-- tests/unit/builder-railpack.bats | 8 ++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/deployment/builders/railpack.md b/docs/deployment/builders/railpack.md index 3483c7d72..dc0d634fb 100644 --- a/docs/deployment/builders/railpack.md +++ b/docs/deployment/builders/railpack.md @@ -9,9 +9,18 @@ The `railpack` builder builds apps via [Railpack](https://railpack.com/), a buil ### Requirements -The `railpack` cli tool is not included by default with Dokku or as a dependency. It must also be installed as shown on [this page](https://railpack.com/installation). +Before using Railpacks, the following steps must be taken: -Builds will proceed with the `railpack` cli for the app from then on. +- Install the `railpack` cli: The `railpack` cli tool is not included by default with Dokku or as a dependency. It must also be installed as shown on [this page](https://railpack.com/installation). +- Create a `buildkit` builder: Railpack uses buildkit. + ```shell + docker run --rm --privileged -d --name buildkit moby/buildkit + ``` +- Set the buildkit builder: Update the `/etc/default/dokku` file to set `BUILDKIT_HOST`: + ```shell + touch /etc/default/dokku + echo "export BUILDKIT_HOST='docker-container://buildkit'" >> /etc/default/dokku + ```` ### Detection diff --git a/tests/unit/builder-railpack.bats b/tests/unit/builder-railpack.bats index 017123f0c..069960a07 100644 --- a/tests/unit/builder-railpack.bats +++ b/tests/unit/builder-railpack.bats @@ -4,6 +4,14 @@ load test_helper setup_file() { install_railpack + docker run --rm --privileged -d --name buildkit moby/buildkit:latest + touch /etc/default/dokku + sudo tee -a /etc/default/dokku <<<"export BUILDKIT_HOST='docker-container://buildkit'" +} + +teardown_file() { + docker container stop buildkit || true + docker container rm buildkit || true } setup() { From b21b58ed3850dad34a1777b3ec43a945a8180302 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 15 Sep 2025 00:21:58 -0400 Subject: [PATCH 019/144] fix: remove extra _ --- tests/unit/scheduler-k3s-1.bats | 2 +- tests/unit/scheduler-k3s-2.bats | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/scheduler-k3s-1.bats b/tests/unit/scheduler-k3s-1.bats index f9fbcd455..b89be6d0e 100644 --- a/tests/unit/scheduler-k3s-1.bats +++ b/tests/unit/scheduler-k3s-1.bats @@ -11,7 +11,7 @@ setup() { export KUBECONFIG="/etc/rancher/k3s/k3s.yaml" } -teardown_() { +teardown() { global_teardown dokku nginx:start uninstall_k3s || true diff --git a/tests/unit/scheduler-k3s-2.bats b/tests/unit/scheduler-k3s-2.bats index b1c47e881..978bfa531 100644 --- a/tests/unit/scheduler-k3s-2.bats +++ b/tests/unit/scheduler-k3s-2.bats @@ -11,7 +11,7 @@ setup() { export KUBECONFIG="/etc/rancher/k3s/k3s.yaml" } -teardown_() { +teardown() { global_teardown dokku nginx:start uninstall_k3s || true From 795e4f0f34be60fc2e1c6afc81f602810baddc5a Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 16 Sep 2025 22:09:07 -0400 Subject: [PATCH 020/144] fix: cleanup build image in railpack builder Without this, it is possible to keep an old build image around in the case of failed builds, and due to missing label support, the image will never be cleaned up. --- plugins/builder-railpack/builder-build | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/builder-railpack/builder-build b/plugins/builder-railpack/builder-build index b64dce3a6..e0716522d 100755 --- a/plugins/builder-railpack/builder-build +++ b/plugins/builder-railpack/builder-build @@ -50,16 +50,19 @@ trigger-builder-railpack-builder-build() { fi # shellcheck disable=SC2086 - if ! railpack build "${ARG_ARRAY[@]}" $RAILPACK_ARGS --name "$IMAGE" "$SOURCECODE_WORK_DIR"; then + if ! railpack build "${ARG_ARRAY[@]}" $RAILPACK_ARGS --name "$IMAGE-build" "$SOURCECODE_WORK_DIR"; then dokku_log_warn "Failure building image" return 1 fi - if ! suppress_output "$DOCKER_BIN" image build -f "$PLUGIN_AVAILABLE_PATH/builder-railpack/dockerfiles/builder-build.Dockerfile" --build-arg APP_IMAGE="$IMAGE" "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -t "$IMAGE" "$SOURCECODE_WORK_DIR"; then + if ! suppress_output "$DOCKER_BIN" image build -f "$PLUGIN_AVAILABLE_PATH/builder-railpack/dockerfiles/builder-build.Dockerfile" --build-arg APP_IMAGE="$IMAGE-build" "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -t "$IMAGE" "$SOURCECODE_WORK_DIR"; then + "$DOCKER_BIN" image remove "$IMAGE-build" dokku_log_warn "Failure injecting docker labels and custom entrypoint on image" return 1 fi + "$DOCKER_BIN" image remove "$IMAGE-build" + plugn trigger post-build "$BUILDER_TYPE" "$APP" "$SOURCECODE_WORK_DIR" popd &>/dev/null || pushd "/tmp" >/dev/null } From b9025f61372a2457cf93f323846f036f7181057a Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 16 Sep 2025 22:09:23 -0400 Subject: [PATCH 021/144] fix: drop unnecessary entrypoint --- .../builder-railpack/dockerfiles/builder-build.Dockerfile | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile b/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile index 57aa80d1c..cca256f21 100644 --- a/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile +++ b/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile @@ -1,7 +1,2 @@ ARG APP_IMAGE FROM $APP_IMAGE - -RUN printf '#!/usr/bin/env bash\nexec bash -l -c -- \"$*\"\n' > /usr/local/bin/entrypoint && \ - chmod +x /usr/local/bin/entrypoint - -ENTRYPOINT ["/usr/local/bin/entrypoint"] From 5e85038d9c6fd92667bc94e93c023242753e1056 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 16 Sep 2025 22:14:12 -0400 Subject: [PATCH 022/144] feat: add PrintCommand support to all task executors --- plugins/common/exec.go | 5 ++++- plugins/common/plugn.go | 5 ++++- plugins/common/sftp.go | 2 +- plugins/common/ssh.go | 3 ++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/plugins/common/exec.go b/plugins/common/exec.go index c5790ba6e..367449b8b 100644 --- a/plugins/common/exec.go +++ b/plugins/common/exec.go @@ -29,6 +29,9 @@ type ExecCommandInput struct { // Env is the environment variables to pass to the command Env map[string]string + // PrintCommand prints the command before executing + PrintCommand bool + // Stdin is the stdin of the command Stdin io.Reader @@ -141,7 +144,7 @@ func CallExecCommandWithContext(ctx context.Context, input ExecCommandInput) (Ex cmd.Cwd = input.WorkingDirectory } - if os.Getenv("DOKKU_TRACE") == "1" { + if input.PrintCommand || os.Getenv("DOKKU_TRACE") == "1" { argsSt := "" if len(cmd.Args) > 0 { argsSt = strings.Join(cmd.Args, " ") diff --git a/plugins/common/plugn.go b/plugins/common/plugn.go index 8ce905ac5..4acf58a21 100644 --- a/plugins/common/plugn.go +++ b/plugins/common/plugn.go @@ -19,6 +19,9 @@ type PlugnTriggerInput struct { // Env is the environment variables to pass to the trigger Env map[string]string + // PrintCommand prints the command before executing + PrintCommand bool + // Stdin is the stdin of the command Stdin io.Reader @@ -55,7 +58,7 @@ func CallPlugnTriggerWithContext(ctx context.Context, input PlugnTriggerInput) ( StreamStderr: input.StreamStderr, }) - if os.Getenv("DOKKU_TRACE") == "1" { + if input.PrintCommand || os.Getenv("DOKKU_TRACE") == "1" { for _, line := range strings.Split(result.Stderr, "\n") { LogDebug(fmt.Sprintf("plugn trigger %s stderr: %s", input.Trigger, line)) } diff --git a/plugins/common/sftp.go b/plugins/common/sftp.go index 1d8ade339..2758ff4e3 100644 --- a/plugins/common/sftp.go +++ b/plugins/common/sftp.go @@ -219,7 +219,7 @@ func (task SftpCopyTask) Execute(ctx context.Context) (SftpCopyResult, error) { }, ctx.Err() } - if task.PrintCommand { + if task.PrintCommand || os.Getenv("DOKKU_TRACE") == "1" { LogDebug(fmt.Sprintf("ssh %s@%s cp %s %v", task.Username, task.Hostname, task.SourcePath, task.DestinationPath)) } diff --git a/plugins/common/ssh.go b/plugins/common/ssh.go index c23f63bc9..f823c75c3 100644 --- a/plugins/common/ssh.go +++ b/plugins/common/ssh.go @@ -203,6 +203,7 @@ type SshTask struct { // PrintCommand prints the command before executing PrintCommand bool + // StreamStdio prints stdout and stderr directly to os.Stdout/err as // the command runs. StreamStdio bool @@ -334,7 +335,7 @@ func (task SshTask) Execute(ctx context.Context) (SshResult, error) { command = "sudo" } - if task.PrintCommand { + if task.PrintCommand || os.Getenv("DOKKU_TRACE") == "1" { LogDebug(fmt.Sprintf("ssh %s@%s %s %v", task.Username, task.Hostname, command, commandArgs)) } From 9af8ec2accc934e58c583b3cb22762d3013bcaff Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 16 Sep 2025 23:07:14 -0400 Subject: [PATCH 023/144] fix: re-add entrypoint It is broken but we still want something like this... --- .../builder-railpack/dockerfiles/builder-build.Dockerfile | 5 +++++ plugins/builder-railpack/dockerfiles/entrypoint | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100755 plugins/builder-railpack/dockerfiles/entrypoint diff --git a/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile b/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile index cca256f21..540af37c5 100644 --- a/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile +++ b/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile @@ -1,2 +1,7 @@ ARG APP_IMAGE FROM $APP_IMAGE + +RUN printf '#!/usr/bin/env bash\nset -x\nexec bash -l -c -- \"$*\"\n' > /usr/local/bin/entrypoint && \ + chmod +x /usr/local/bin/entrypoint + +ENTRYPOINT ["/usr/local/bin/entrypoint"] diff --git a/plugins/builder-railpack/dockerfiles/entrypoint b/plugins/builder-railpack/dockerfiles/entrypoint new file mode 100755 index 000000000..f457dd278 --- /dev/null +++ b/plugins/builder-railpack/dockerfiles/entrypoint @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +echo "#!/usr/bin/env bash" > /tmp/cmd +echo "" >> /tmp/cmd +echo "$@" >> /tmp/cmd +exec bash "$@" From b4fbf7189e1469c32e6b6aea964a045e38d0624b Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 16 Sep 2025 23:39:36 -0400 Subject: [PATCH 024/144] fix: update tests --- tests/unit/builder-railpack.bats | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/builder-railpack.bats b/tests/unit/builder-railpack.bats index 069960a07..d92916ef7 100644 --- a/tests/unit/builder-railpack.bats +++ b/tests/unit/builder-railpack.bats @@ -37,7 +37,7 @@ teardown() { echo "output: $output" echo "status: $status" assert_success - assert_output_contains 'load build definition from Dockerfile' + assert_output_contains 'Successfully built image in' assert_output_contains "SECRET_KEY: fjdkslafjdk" run /bin/bash -c "dokku builder-railpack:set $TEST_APP railpackjson-path nonexistent.json" @@ -49,7 +49,7 @@ teardown() { echo "output: $output" echo "status: $status" assert_success - assert_output_contains 'load build definition from Dockerfile' + assert_output_contains 'Successfully built image in' } inject_requirements_txt() { From 3c6ace7d68740e018e8aeaea39eb5c8c8c08f835 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 16 Sep 2025 23:39:47 -0400 Subject: [PATCH 025/144] tests: add global setup/teardown --- tests/unit/builder-railpack.bats | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/builder-railpack.bats b/tests/unit/builder-railpack.bats index d92916ef7..e0dcc5f6c 100644 --- a/tests/unit/builder-railpack.bats +++ b/tests/unit/builder-railpack.bats @@ -3,6 +3,7 @@ load test_helper setup_file() { + global_setup install_railpack docker run --rm --privileged -d --name buildkit moby/buildkit:latest touch /etc/default/dokku @@ -20,6 +21,7 @@ setup() { teardown() { destroy_app + global_teardown } @test "(builder-railpack:set)" { From e5196a2f0365536ffb577295ab2febfd5a73d200 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 16 Sep 2025 23:40:10 -0400 Subject: [PATCH 026/144] fix: remove entrypoint to ensure we can pass commands as arrays instead of quoted strings --- .../builder-railpack/dockerfiles/builder-build.Dockerfile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile b/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile index 540af37c5..185c7e950 100644 --- a/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile +++ b/plugins/builder-railpack/dockerfiles/builder-build.Dockerfile @@ -1,7 +1,4 @@ ARG APP_IMAGE FROM $APP_IMAGE -RUN printf '#!/usr/bin/env bash\nset -x\nexec bash -l -c -- \"$*\"\n' > /usr/local/bin/entrypoint && \ - chmod +x /usr/local/bin/entrypoint - -ENTRYPOINT ["/usr/local/bin/entrypoint"] +ENTRYPOINT [] From f2abbad5b793a6d762bbd0ce5d958fc702f8a81d Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 16 Sep 2025 23:51:58 -0400 Subject: [PATCH 027/144] chore: remove unused entrypoint file --- plugins/builder-railpack/dockerfiles/entrypoint | 6 ------ 1 file changed, 6 deletions(-) delete mode 100755 plugins/builder-railpack/dockerfiles/entrypoint diff --git a/plugins/builder-railpack/dockerfiles/entrypoint b/plugins/builder-railpack/dockerfiles/entrypoint deleted file mode 100755 index f457dd278..000000000 --- a/plugins/builder-railpack/dockerfiles/entrypoint +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -echo "#!/usr/bin/env bash" > /tmp/cmd -echo "" >> /tmp/cmd -echo "$@" >> /tmp/cmd -exec bash "$@" From 551eae0c5b285b1a242459974866905cf2eafb9a Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 14 Sep 2025 20:59:57 -0400 Subject: [PATCH 028/144] chore: remove references to pack being experimental It is no longer experimental and is here to stay. --- .../builders/cloud-native-buildpacks.md | 17 ++++------------- plugins/builder-pack/builder-detect | 5 ----- plugins/common/functions | 5 ----- tests/unit/app-json.bats | 7 ++++++- tests/unit/builder.bats | 13 ------------- tests/unit/ps-cnb.bats | 4 ++-- tests/unit/run_1.bats | 7 ++++++- 7 files changed, 18 insertions(+), 40 deletions(-) diff --git a/docs/deployment/builders/cloud-native-buildpacks.md b/docs/deployment/builders/cloud-native-buildpacks.md index 83a3ec407..f6a3ebb83 100644 --- a/docs/deployment/builders/cloud-native-buildpacks.md +++ b/docs/deployment/builders/cloud-native-buildpacks.md @@ -14,8 +14,8 @@ buildpacks:set-property [--global|] # Set or clear a buildpa Cloud Native Buildpacks are an evolution over the Buildpacks technology provided by the Herokuish builder. See the [herokuish buildpacks documentation](/docs/deployment/builders/herokuish-buildpacks.md) for more information on how to clear buildpack build cache for an application. -> [!WARNING] -> This functionality uses the `pack` cli from the [Cloud Native Buildpacks](https://buildpacks.io) project to build apps. As the integration is experimental in Dokku, it is likely to change over time. +> [!NOTE] +> This functionality uses the `pack` cli from the [Cloud Native Buildpacks](https://buildpacks.io) project to build apps. ## Usage @@ -27,7 +27,7 @@ Builds will proceed with the `pack` cli for the app from then on. ### Caveats -As this functionality is highly experimental, there are a number of caveats. Please note that not all issues are listed below. +While pack is well-supported in Dokku, there are a number of caveats. Please note that not all issues are listed below. - Specifying specific buildpacks is not currently possible. - A future release will add support for specifying buildpacks via the `buildpacks` plugin. @@ -41,16 +41,7 @@ As this functionality is highly experimental, there are a number of caveats. Ple ### Detection -This builder will be auto-detected in either the following cases: - -- The `DOKKU_CNB_EXPERIMENTAL` app environment variable is set to `1`. - - ```shell - dokku config:set --no-restart node-js-app DOKKU_CNB_EXPERIMENTAL=1 - ``` - -- A `project.toml` file exists in the root of the app repository. - - This file is consumed by `pack-cli` and used to describe how the app is built. +This builder will be auto-detected if a `project.toml` file exists in the root of the app repository. This file is consumed by `pack-cli` and used to describe how the app is built. The builder can also be specified via the `builder:set` command: diff --git a/plugins/builder-pack/builder-detect b/plugins/builder-pack/builder-detect index d269c11dc..4202263ef 100755 --- a/plugins/builder-pack/builder-detect +++ b/plugins/builder-pack/builder-detect @@ -12,11 +12,6 @@ trigger-builder-pack-builder-detect() { echo "pack" return fi - - if [[ "$(config_get "$APP" DOKKU_CNB_EXPERIMENTAL || true)" == "1" ]]; then - echo "pack" - return - fi } trigger-builder-pack-builder-detect "$@" diff --git a/plugins/common/functions b/plugins/common/functions index ea1dcfe06..9a7e12c84 100755 --- a/plugins/common/functions +++ b/plugins/common/functions @@ -665,11 +665,6 @@ dokku_receive() { docker_cleanup "$APP" - local DOKKU_CNB_EXPERIMENTAL="$(config_get "$APP" DOKKU_CNB_EXPERIMENTAL || true)" - if [[ "$DOKKU_CNB_EXPERIMENTAL" == "1" ]]; then - IMAGE_SOURCE_TYPE="pack" - fi - plugn trigger builder-set-property "$APP" "detected" "$IMAGE_SOURCE_TYPE" if ! dokku_build "$APP" "$IMAGE_SOURCE_TYPE" "$TMP_WORK_DIR"; then return 1 diff --git a/tests/unit/app-json.bats b/tests/unit/app-json.bats index acdd54854..ba8325f6a 100644 --- a/tests/unit/app-json.bats +++ b/tests/unit/app-json.bats @@ -94,7 +94,12 @@ teardown() { echo "status: $status" assert_success - run /bin/bash -c "dokku config:set --no-restart $TEST_APP DOKKU_CNB_EXPERIMENTAL=1 SECRET_KEY=fjdkslafjdk" + run /bin/bash -c "dokku config:set --no-restart $TEST_APP SECRET_KEY=fjdkslafjdk" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku builder:set $TEST_APP selected pack" echo "output: $output" echo "status: $status" assert_success diff --git a/tests/unit/builder.bats b/tests/unit/builder.bats index 8d6907fd6..6cbaa790e 100644 --- a/tests/unit/builder.bats +++ b/tests/unit/builder.bats @@ -99,19 +99,6 @@ teardown() { 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" - echo "output: $output" - echo "status: $status" - assert_success - assert_line 0 "pack" } @test "(builder) builder-detect [dockerfile]" { diff --git a/tests/unit/ps-cnb.bats b/tests/unit/ps-cnb.bats index c24044428..25fe17e42 100644 --- a/tests/unit/ps-cnb.bats +++ b/tests/unit/ps-cnb.bats @@ -17,7 +17,7 @@ teardown() { } @test "(ps) cnb env vars" { - run /bin/bash -c "dokku config:set $TEST_APP DOKKU_CNB_EXPERIMENTAL=1" + run /bin/bash -c "dokku config:set $TEST_APP APP_RESPECTS_ENV_VARS=1" echo "output: $output" echo "status: $status" assert_success @@ -31,5 +31,5 @@ teardown() { echo "output: $output" echo "status: $status" assert_success - assert_output_contains '"DOKKU_CNB_EXPERIMENTAL": "1"' + assert_output_contains '"APP_RESPECTS_ENV_VARS": "1"' } diff --git a/tests/unit/run_1.bats b/tests/unit/run_1.bats index 91574f126..913b6de2c 100644 --- a/tests/unit/run_1.bats +++ b/tests/unit/run_1.bats @@ -68,7 +68,12 @@ teardown() { echo "status: $status" assert_success - run /bin/bash -c "dokku config:set --no-restart $TEST_APP DOKKU_CNB_EXPERIMENTAL=1 SECRET_KEY=fjdkslafjdk" + run /bin/bash -c "dokku config:set --no-restart $TEST_APP SECRET_KEY=fjdkslafjdk" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku builder:set $TEST_APP selected pack" echo "output: $output" echo "status: $status" assert_success From e62212f810e794815c13eabd88c06700899f7da9 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 17 Sep 2025 00:57:54 -0400 Subject: [PATCH 029/144] tests: add missing global setup/teardown calls --- tests/unit/builder-pack.bats | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/builder-pack.bats b/tests/unit/builder-pack.bats index 489c7f96f..924f15538 100644 --- a/tests/unit/builder-pack.bats +++ b/tests/unit/builder-pack.bats @@ -7,11 +7,13 @@ setup_file() { } setup() { + global_setup create_app } teardown() { destroy_app + global_teardown } @test "(builder-pack:set)" { From 68c5407ed4df66f0a8475f47e2c2c09fb1eea13e Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 17 Sep 2025 00:58:30 -0400 Subject: [PATCH 030/144] tests: use already existing add_requirements_txt_cnb function for builder-pack deploys --- tests/unit/builder-pack.bats | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/unit/builder-pack.bats b/tests/unit/builder-pack.bats index 924f15538..3becd43a6 100644 --- a/tests/unit/builder-pack.bats +++ b/tests/unit/builder-pack.bats @@ -32,7 +32,7 @@ teardown() { echo "status: $status" assert_success - run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP initialize_for_cnb + run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP add_requirements_txt_cnb echo "output: $output" echo "status: $status" assert_success @@ -82,7 +82,7 @@ teardown() { echo "status: $status" assert_success - run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP initialize_for_cnb + run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP add_requirements_txt_cnb echo "output: $output" echo "status: $status" assert_success @@ -103,11 +103,3 @@ teardown() { echo "status: $status" assert_success } - -initialize_for_cnb() { - local APP="$1" - local APP_REPO_DIR="$2" - [[ -z "$APP" ]] && local APP="$TEST_APP" - echo "flask" >>"$APP_REPO_DIR/requirements.txt" - mv "$APP_REPO_DIR/app-cnb.json" "$APP_REPO_DIR/app.json" -} From 889d237669823b9dfcd32237fd42361ef360fce7 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 17 Sep 2025 00:58:46 -0400 Subject: [PATCH 031/144] chore: combine tests and ensure that pack is selected --- tests/unit/builder-pack.bats | 23 +++++++++++++++++++++++ tests/unit/ps-cnb.bats | 35 ----------------------------------- 2 files changed, 23 insertions(+), 35 deletions(-) delete mode 100644 tests/unit/ps-cnb.bats diff --git a/tests/unit/builder-pack.bats b/tests/unit/builder-pack.bats index 3becd43a6..fad2052fb 100644 --- a/tests/unit/builder-pack.bats +++ b/tests/unit/builder-pack.bats @@ -103,3 +103,26 @@ teardown() { echo "status: $status" assert_success } + +@test "(ps) cnb env vars" { + run /bin/bash -c "dokku config:set $TEST_APP APP_RESPECTS_ENV_VARS=1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku builder:set $TEST_APP selected pack" + echo "output: $output" + echo "status: $status" + assert_success + + run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP add_requirements_txt_cnb + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "curl $(dokku url $TEST_APP)/env" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains '"APP_RESPECTS_ENV_VARS": "1"' +} diff --git a/tests/unit/ps-cnb.bats b/tests/unit/ps-cnb.bats deleted file mode 100644 index 25fe17e42..000000000 --- a/tests/unit/ps-cnb.bats +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bats - -load test_helper - -setup_file() { - install_pack -} - -setup() { - global_setup - create_app -} - -teardown() { - destroy_app - global_teardown -} - -@test "(ps) cnb env vars" { - run /bin/bash -c "dokku config:set $TEST_APP APP_RESPECTS_ENV_VARS=1" - echo "output: $output" - echo "status: $status" - assert_success - - run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP add_requirements_txt_cnb - echo "output: $output" - echo "status: $status" - assert_success - - run /bin/bash -c "curl $(dokku url $TEST_APP)/env" - echo "output: $output" - echo "status: $status" - assert_success - assert_output_contains '"APP_RESPECTS_ENV_VARS": "1"' -} From c885205b913f54fcef0e15709591845fc203d26c Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 21 Sep 2025 19:48:11 -0400 Subject: [PATCH 032/144] refactor: respect the maintenance value of the cron task instead of skipping them The former method would omit the CronJob objects entirely, meaning a redeploy would be necessary to re-enable them. --- plugins/scheduler-k3s/template.go | 1 + plugins/scheduler-k3s/templates/chart/cron-job.yaml | 2 +- plugins/scheduler-k3s/triggers.go | 10 ++-------- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/plugins/scheduler-k3s/template.go b/plugins/scheduler-k3s/template.go index 835e5f35d..17eafdde5 100644 --- a/plugins/scheduler-k3s/template.go +++ b/plugins/scheduler-k3s/template.go @@ -302,6 +302,7 @@ type ProcessCron struct { ID string `yaml:"id"` Schedule string `yaml:"schedule"` Suffix string `yaml:"suffix"` + Suspend bool `yaml:"suspend"` } type ProcessPortMap struct { diff --git a/plugins/scheduler-k3s/templates/chart/cron-job.yaml b/plugins/scheduler-k3s/templates/chart/cron-job.yaml index f44d40696..b5b4e8b88 100644 --- a/plugins/scheduler-k3s/templates/chart/cron-job.yaml +++ b/plugins/scheduler-k3s/templates/chart/cron-job.yaml @@ -135,6 +135,6 @@ spec: schedule: {{ $config.cron.schedule }} startingDeadlineSeconds: 60 successfulJobsHistoryLimit: 10 - suspend: false + suspend: {{ $config.cron.suspend }} timeZone: Etc/UTC {{- end }} diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index def82453f..efaba74b0 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -312,17 +312,10 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e workingDir := common.GetWorkingDir(appName, image) - allCronTasks, err := cron.FetchCronTasks(cron.FetchCronTasksInput{AppName: appName}) + cronTasks, err := cron.FetchCronTasks(cron.FetchCronTasksInput{AppName: appName}) if err != nil { return fmt.Errorf("Error fetching cron tasks: %w", err) } - // remove maintenance cron tasks - cronTasks := []cron.CronTask{} - for _, cronTask := range allCronTasks { - if !cronTask.Maintenance { - cronTasks = append(cronTasks, cronTask) - } - } domains := []string{} if _, ok := processes["web"]; ok { @@ -655,6 +648,7 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e ID: cronTask.ID, Schedule: cronTask.Schedule, Suffix: suffix, + Suspend: cronEntry.Maintenance, }, Labels: labels, ProcessType: ProcessType_Cron, From 4a357e9896f24740d5f3f3eeb7c0ada5c52ea8ec Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 21 Sep 2025 20:08:22 -0400 Subject: [PATCH 033/144] fix: properly suspend and resume k8s CronJob objects when executing scheduler-cron-write --- plugins/scheduler-k3s/Makefile | 2 +- plugins/scheduler-k3s/k8s.go | 29 ++++++++++ .../scheduler-k3s/src/triggers/triggers.go | 4 ++ plugins/scheduler-k3s/triggers.go | 58 +++++++++++++++++++ 4 files changed, 92 insertions(+), 1 deletion(-) diff --git a/plugins/scheduler-k3s/Makefile b/plugins/scheduler-k3s/Makefile index e894570d5..b873f3701 100644 --- a/plugins/scheduler-k3s/Makefile +++ b/plugins/scheduler-k3s/Makefile @@ -1,5 +1,5 @@ SUBCOMMANDS = subcommands/annotations:set subcommands/autoscaling-auth:set subcommands/autoscaling-auth:report subcommands/cluster-add subcommands/cluster-list subcommands/cluster-remove subcommands/ensure-charts subcommands/initialize subcommands/labels:set subcommands/report subcommands/set subcommands/show-kubeconfig subcommands/uninstall -TRIGGERS = triggers/core-post-deploy triggers/core-post-extract triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-create triggers/post-delete triggers/report triggers/scheduler-app-status triggers/scheduler-deploy triggers/scheduler-enter triggers/scheduler-is-deployed triggers/scheduler-logs triggers/scheduler-proxy-config triggers/scheduler-proxy-logs triggers/scheduler-post-delete triggers/scheduler-run triggers/scheduler-run-list triggers/scheduler-stop +TRIGGERS = triggers/core-post-deploy triggers/core-post-extract triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-create triggers/post-delete triggers/report triggers/scheduler-app-status triggers/scheduler-deploy triggers/scheduler-enter triggers/scheduler-is-deployed triggers/scheduler-logs triggers/scheduler-proxy-config triggers/scheduler-proxy-logs triggers/scheduler-post-delete triggers/scheduler-run triggers/scheduler-run-list triggers/scheduler-stop triggers/scheduler-cron-write BUILD = commands subcommands triggers PLUGIN_NAME = scheduler-k3s diff --git a/plugins/scheduler-k3s/k8s.go b/plugins/scheduler-k3s/k8s.go index a078f8ead..aa5a07543 100644 --- a/plugins/scheduler-k3s/k8s.go +++ b/plugins/scheduler-k3s/k8s.go @@ -805,6 +805,35 @@ func (k KubernetesClient) ListTriggerAuthentications(ctx context.Context, input return triggerAuthentications, nil } +// ResumeCronJobsInput contains all the information needed to resume a Kubernetes cron job +type ResumeCronJobsInput struct { + // LabelSelector is the Kubernetes label selector + LabelSelector string + + // Namespace is the Kubernetes namespace + Namespace string +} + +// ResumeCronJobs resumes a Kubernetes cron job +func (k KubernetesClient) ResumeCronJobs(ctx context.Context, input ResumeCronJobsInput) error { + cronJobs, err := k.Client.BatchV1().CronJobs(input.Namespace).List(ctx, metav1.ListOptions{ + LabelSelector: input.LabelSelector, + }) + if err != nil { + return err + } + + for _, cronJob := range cronJobs.Items { + cronJob.Spec.Suspend = ptr.To(false) + _, err := k.Client.BatchV1().CronJobs(input.Namespace).Update(ctx, &cronJob, metav1.UpdateOptions{}) + if err != nil { + return err + } + } + + return nil +} + // ScaleDeploymentInput contains all the information needed to scale a Kubernetes deployment type ScaleDeploymentInput struct { // Name is the Kubernetes deployment name diff --git a/plugins/scheduler-k3s/src/triggers/triggers.go b/plugins/scheduler-k3s/src/triggers/triggers.go index dd48a6b71..15caf5146 100644 --- a/plugins/scheduler-k3s/src/triggers/triggers.go +++ b/plugins/scheduler-k3s/src/triggers/triggers.go @@ -123,6 +123,10 @@ func main() { scheduler := flag.Arg(0) appName := flag.Arg(1) err = scheduler_k3s.TriggerSchedulerStop(scheduler, appName) + case "scheduler-cron-write": + scheduler := flag.Arg(0) + appName := flag.Arg(1) + err = scheduler_k3s.TriggerSchedulerCronWrite(scheduler, appName) case "scheduler-post-delete": scheduler := flag.Arg(0) appName := flag.Arg(1) diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index efaba74b0..4ca07841f 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -163,6 +163,64 @@ func TriggerSchedulerAppStatus(scheduler string, appName string) error { return nil } +// TriggerSchedulerCronWrite writes out cron entries for a given application +func TriggerSchedulerCronWrite(scheduler string, appName string) error { + if scheduler != "k3s" { + return nil + } + + allCronEntries, err := cron.FetchCronEntries(cron.FetchCronEntriesInput{AppName: appName}) + if err != nil { + return fmt.Errorf("Error fetching cron entries: %w", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + signals := make(chan os.Signal, 1) + signal.Notify(signals, os.Interrupt, syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGQUIT, + syscall.SIGTERM) + go func() { + <-signals + common.LogWarn(fmt.Sprintf("Deployment of %s has been cancelled", appName)) + cancel() + }() + + namespace := getComputedNamespace(appName) + + clientset, err := NewKubernetesClient() + if err != nil { + return fmt.Errorf("Error creating kubernetes client: %w", err) + } + + for _, cronEntry := range allCronEntries { + labelSelector := []string{ + fmt.Sprintf("app.kubernetes.io/part-of=%s", appName), + fmt.Sprintf("dokku.com/cron-id=%s", cronEntry.ID), + } + + if cronEntry.Maintenance { + err = clientset.SuspendCronJobs(ctx, SuspendCronJobsInput{ + Namespace: namespace, + LabelSelector: strings.Join(labelSelector, ","), + }) + if err != nil { + return fmt.Errorf("Error suspending cron jobs: %w", err) + } + } else { + err = clientset.ResumeCronJobs(ctx, ResumeCronJobsInput{ + Namespace: namespace, + LabelSelector: strings.Join(labelSelector, ","), + }) + if err != nil { + return fmt.Errorf("Error resuming cron jobs: %w", err) + } + } + } + + return nil +} + // TriggerSchedulerDeploy deploys an image tag for a given application func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) error { if scheduler != "k3s" { From e42df29a790ab9711d66afa7bc3b2725f7a4f5d4 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 21 Sep 2025 20:14:50 -0400 Subject: [PATCH 034/144] feat: allow specifying maintenance mode in the file or not If the app's cron tasks are set to maintenance mode, then the task is set to maintenance mode regardless. --- docs/appendices/file-formats/app-json.md | 1 + docs/processes/scheduled-cron-tasks.md | 1 + plugins/app-json/appjson.go | 3 +++ plugins/cron/cron.go | 5 +++-- plugins/scheduler-k3s/triggers.go | 10 +++++----- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/appendices/file-formats/app-json.md b/docs/appendices/file-formats/app-json.md index 189a892b8..c7500e19f 100644 --- a/docs/appendices/file-formats/app-json.md +++ b/docs/appendices/file-formats/app-json.md @@ -21,6 +21,7 @@ (list, optional) A list of cron resources. Keys are the names of the process types. The values are an object containing one or more of the following properties: - `command`: (string, required) +- `maintenance`: (boolean, optional) - `schedule`: (string, required) ## Formation diff --git a/docs/processes/scheduled-cron-tasks.md b/docs/processes/scheduled-cron-tasks.md index f647b6edb..a8d39fdfc 100644 --- a/docs/processes/scheduled-cron-tasks.md +++ b/docs/processes/scheduled-cron-tasks.md @@ -34,6 +34,7 @@ The `app.json` file for a given app can define a special `cron` key that contain A cron task takes the following properties: - `command`: A command to be run within the built app image. Specified commands can also be `Procfile` entries. +- `maintenance`: A boolean value that decides whether the cron task is in maintenance and therefore executable or not. - `schedule`: A [cron-compatible](https://en.wikipedia.org/wiki/Cron#Overview) scheduling definition upon which to run the command. Seconds are generally not supported. Zero or more cron tasks can be specified per app. Cron tasks are validated after the build artifact is created but before the app is deployed, and the cron schedule is updated during the post-deploy phase. diff --git a/plugins/app-json/appjson.go b/plugins/app-json/appjson.go index d251da4ea..ab80fb21f 100644 --- a/plugins/app-json/appjson.go +++ b/plugins/app-json/appjson.go @@ -55,6 +55,9 @@ type CronTask struct { // Command is the command to execute Command string `json:"command"` + // Maintenance is whether or not the cron command is in maintenance mode + Maintenance bool `json:"maintenance"` + // Schedule is the cron schedule to execute the command on Schedule string `json:"schedule"` } diff --git a/plugins/cron/cron.go b/plugins/cron/cron.go index 9fb96860c..90cbdced7 100644 --- a/plugins/cron/cron.go +++ b/plugins/cron/cron.go @@ -77,7 +77,7 @@ type FetchCronTasksInput struct { func FetchCronTasks(input FetchCronTasksInput) ([]CronTask, error) { appName := input.AppName tasks := []CronTask{} - isMaintenance := reportComputedMaintenance(appName) == "true" + isAppCronInMaintenance := reportComputedMaintenance(appName) == "true" if input.AppJSON == nil && input.AppName == "" { return tasks, fmt.Errorf("Missing app name or app.json") @@ -121,12 +121,13 @@ func FetchCronTasks(input FetchCronTasksInput) ([]CronTask, error) { return tasks, fmt.Errorf("Invalid cron schedule for app %s (schedule %s): %s", appName, c.Schedule, err.Error()) } + maintenance := isAppCronInMaintenance || c.Maintenance tasks = append(tasks, CronTask{ App: appName, Command: c.Command, Schedule: c.Schedule, ID: GenerateCommandID(appName, c), - Maintenance: isMaintenance, + Maintenance: maintenance, }) } diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index 4ca07841f..7a2e0e030 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -169,7 +169,7 @@ func TriggerSchedulerCronWrite(scheduler string, appName string) error { return nil } - allCronEntries, err := cron.FetchCronEntries(cron.FetchCronEntriesInput{AppName: appName}) + cronTasks, err := cron.FetchCronTasks(cron.FetchCronTasksInput{AppName: appName}) if err != nil { return fmt.Errorf("Error fetching cron entries: %w", err) } @@ -193,13 +193,13 @@ func TriggerSchedulerCronWrite(scheduler string, appName string) error { return fmt.Errorf("Error creating kubernetes client: %w", err) } - for _, cronEntry := range allCronEntries { + for _, cronTask := range cronTasks { labelSelector := []string{ fmt.Sprintf("app.kubernetes.io/part-of=%s", appName), - fmt.Sprintf("dokku.com/cron-id=%s", cronEntry.ID), + fmt.Sprintf("dokku.com/cron-id=%s", cronTask.ID), } - if cronEntry.Maintenance { + if cronTask.Maintenance { err = clientset.SuspendCronJobs(ctx, SuspendCronJobsInput{ Namespace: namespace, LabelSelector: strings.Join(labelSelector, ","), @@ -706,7 +706,7 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e ID: cronTask.ID, Schedule: cronTask.Schedule, Suffix: suffix, - Suspend: cronEntry.Maintenance, + Suspend: cronTask.Maintenance, }, Labels: labels, ProcessType: ProcessType_Cron, From 3c7985cd82675f5389947c4379cd83cd5858a662 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 21 Sep 2025 20:30:49 -0400 Subject: [PATCH 035/144] feat: track task vs app maintenance separately We should know _where_ the maintenance value is applied for the end-user's usage --- plugins/cron/cron.go | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/plugins/cron/cron.go b/plugins/cron/cron.go index 90cbdced7..dbca27826 100644 --- a/plugins/cron/cron.go +++ b/plugins/cron/cron.go @@ -50,7 +50,13 @@ type CronTask struct { // LogFile is the log file to write to LogFile string `json:"-"` - // Maintenance is whether the cron task is in maintenance mode + // AppInMaintenance is whether the app's cron is in maintenance mode + AppInMaintenance bool `json:"app-in-maintenance"` + + // Maintenance is whether the cron command is in maintenance mode + TaskInMaintenance bool `json:"task-in-maintenance"` + + // Maintenance is whether the cron command is in maintenance mode Maintenance bool `json:"maintenance"` } @@ -123,11 +129,13 @@ func FetchCronTasks(input FetchCronTasksInput) ([]CronTask, error) { maintenance := isAppCronInMaintenance || c.Maintenance tasks = append(tasks, CronTask{ - App: appName, - Command: c.Command, - Schedule: c.Schedule, - ID: GenerateCommandID(appName, c), - Maintenance: maintenance, + App: appName, + Command: c.Command, + Schedule: c.Schedule, + ID: GenerateCommandID(appName, c), + Maintenance: isAppCronInMaintenance || maintenance, + AppInMaintenance: isAppCronInMaintenance, + TaskInMaintenance: maintenance, }) } @@ -156,12 +164,14 @@ func FetchGlobalCronTasks() ([]CronTask, error) { id := base36.EncodeToStringLc([]byte(strings.Join(parts, ";;;"))) task := CronTask{ - ID: id, - Schedule: parts[0], - Command: parts[1], - AltCommand: parts[1], - Maintenance: false, - Global: true, + ID: id, + Schedule: parts[0], + Command: parts[1], + AltCommand: parts[1], + Global: true, + Maintenance: false, + TaskInMaintenance: false, + AppInMaintenance: false, } if len(parts) == 3 { task.LogFile = parts[2] From f99e3a6c9c3ffd6460a6bf2352ec7d4cc55ce406 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 21 Sep 2025 20:52:57 -0400 Subject: [PATCH 036/144] feat: add ability to suspend and resume a cron task by ID This takes advantage of the properties system and allows users to suspend/resume the task at will. --- plugins/common/properties.go | 14 ++--- plugins/cron/Makefile | 2 +- plugins/cron/cron.go | 22 +++++++- plugins/cron/src/subcommands/subcommands.go | 12 +++++ plugins/cron/subcommands.go | 58 ++++++++++++++++++++- plugins/scheduler-k3s/triggers.go | 4 +- 6 files changed, 99 insertions(+), 13 deletions(-) diff --git a/plugins/common/properties.go b/plugins/common/properties.go index 637991d77..b472c24b9 100644 --- a/plugins/common/properties.go +++ b/plugins/common/properties.go @@ -12,27 +12,27 @@ import ( ) // CommandPropertySet is a generic function that will set a property for a given plugin/app combination -func CommandPropertySet(pluginName, appName, property, value string, properties map[string]string, globalProperties map[string]bool) { +func CommandPropertySet(pluginName, appName, property, value string, validProperties map[string]string, validGlobalProperties map[string]bool) { if appName != "--global" { if err := VerifyAppName(appName); err != nil { LogFailWithError(err) } } - if appName == "--global" && !globalProperties[property] { + if appName == "--global" && !validGlobalProperties[property] { LogFail("Property cannot be specified globally") } if property == "" { LogFail("No property specified") } - for k := range globalProperties { - if _, ok := properties[k]; !ok { - properties[k] = "" + for k := range validGlobalProperties { + if _, ok := validProperties[k]; !ok { + validProperties[k] = "" } } - if _, ok := properties[property]; !ok { - properties := reflect.ValueOf(properties).MapKeys() + if _, ok := validProperties[property]; !ok { + properties := reflect.ValueOf(validProperties).MapKeys() validPropertyList := make([]string, len(properties)) for i := 0; i < len(properties); i++ { validPropertyList[i] = properties[i].String() diff --git a/plugins/cron/Makefile b/plugins/cron/Makefile index 98eeb5fd2..46513788c 100644 --- a/plugins/cron/Makefile +++ b/plugins/cron/Makefile @@ -1,4 +1,4 @@ -SUBCOMMANDS = subcommands/list subcommands/report subcommands/run subcommands/set +SUBCOMMANDS = subcommands/list subcommands/report cron/resume subcommands/run subcommands/set subcommands/suspend TRIGGERS = triggers/app-json-is-valid triggers/cron-get-property triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-delete triggers/scheduler-stop BUILD = commands subcommands triggers PLUGIN_NAME = cron diff --git a/plugins/cron/cron.go b/plugins/cron/cron.go index dbca27826..de19d07b3 100644 --- a/plugins/cron/cron.go +++ b/plugins/cron/cron.go @@ -2,6 +2,7 @@ package cron import ( "fmt" + "strconv" "strings" appjson "github.com/dokku/dokku/plugins/app-json" @@ -27,6 +28,8 @@ var ( } ) +const MaintenancePropertyPrefix = "maintenance." + // CronTask is a struct that represents a cron task type CronTask struct { // ID is a unique identifier for the cron task @@ -102,6 +105,11 @@ func FetchCronTasks(input FetchCronTasksInput) ([]CronTask, error) { return tasks, nil } + properties, err := common.PropertyGetAllByPrefix("cron", appName, MaintenancePropertyPrefix) + if err != nil { + return tasks, fmt.Errorf("Error getting maintenance properties: %w", err) + } + for i, c := range input.AppJSON.Cron { if c.Command == "" { if input.WarnToFailure { @@ -127,7 +135,19 @@ func FetchCronTasks(input FetchCronTasksInput) ([]CronTask, error) { return tasks, fmt.Errorf("Invalid cron schedule for app %s (schedule %s): %s", appName, c.Schedule, err.Error()) } - maintenance := isAppCronInMaintenance || c.Maintenance + maintenance := c.Maintenance + if value, ok := properties[MaintenancePropertyPrefix+c.Schedule]; ok { + boolValue, err := strconv.ParseBool(value) + if err != nil { + return tasks, fmt.Errorf("Invalid maintenance property for app %s (schedule %s): %s", appName, c.Schedule, err.Error()) + } + + // only override the maintenance value if the property is set to true + if boolValue { + maintenance = boolValue + } + } + tasks = append(tasks, CronTask{ App: appName, Command: c.Command, diff --git a/plugins/cron/src/subcommands/subcommands.go b/plugins/cron/src/subcommands/subcommands.go index 329b240e8..c9452ee05 100644 --- a/plugins/cron/src/subcommands/subcommands.go +++ b/plugins/cron/src/subcommands/subcommands.go @@ -37,6 +37,12 @@ func main() { appName := args.Arg(0) err = cron.CommandReport(appName, *format, infoFlag) } + case "resume": + args := flag.NewFlagSet("cron:resume", flag.ExitOnError) + args.Parse(os.Args[2:]) + appName := args.Arg(0) + cronID := args.Arg(1) + err = cron.CommandResume(appName, cronID) case "run": args := flag.NewFlagSet("cron:run", flag.ExitOnError) detached := args.Bool("detach", false, "--detach: run the container in a detached mode") @@ -57,6 +63,12 @@ func main() { value = args.Arg(1) } err = cron.CommandSet(appName, property, value) + case "suspend": + args := flag.NewFlagSet("cron:suspend", flag.ExitOnError) + args.Parse(os.Args[2:]) + appName := args.Arg(0) + cronID := args.Arg(1) + err = cron.CommandSuspend(appName, cronID) default: err = fmt.Errorf("Invalid plugin subcommand call: %s", subcommand) } diff --git a/plugins/cron/subcommands.go b/plugins/cron/subcommands.go index 1cc89342f..aef1b833e 100644 --- a/plugins/cron/subcommands.go +++ b/plugins/cron/subcommands.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "strings" "github.com/dokku/dokku/plugins/common" @@ -43,7 +44,15 @@ func CommandList(appName string, format string) error { if format == "stdout" { output := []string{"ID | Schedule | Maintenance | Command"} for _, task := range tasks { - output = append(output, fmt.Sprintf("%s | %s | %t | %s", task.ID, task.Schedule, task.Maintenance, task.Command)) + maintenance := "false" + if task.Maintenance { + if task.TaskInMaintenance { + maintenance = "true (task)" + } else if task.AppInMaintenance { + maintenance = "true (app)" + } + } + output = append(output, fmt.Sprintf("%s | %s | %s | %s", task.ID, task.Schedule, maintenance, task.Command)) } result := columnize.SimpleFormat(output) @@ -82,6 +91,11 @@ func CommandReport(appName string, format string, infoFlag string) error { return ReportSingleApp(appName, format, infoFlag) } +// CommandResume resumes a cron task +func CommandResume(appName string, cronID string) error { + return CommandSet(appName, fmt.Sprintf("%s%s", MaintenancePropertyPrefix, cronID), "") +} + // CommandRun executes a cron task on the fly func CommandRun(appName string, cronID string, detached bool) error { if err := common.VerifyAppName(appName); err != nil { @@ -138,7 +152,42 @@ func CommandSet(appName string, property string, value string) error { return err } - common.CommandPropertySet("cron", appName, property, value, DefaultProperties, GlobalProperties) + validProperties := DefaultProperties + globalProperties := GlobalProperties + if strings.HasPrefix(property, MaintenancePropertyPrefix) { + if appName == "--global" { + return fmt.Errorf("Task maintenance properties cannot be set globally") + } + + maintenanceParts := strings.SplitN(property, ".", 2) + if len(maintenanceParts) != 3 { + return fmt.Errorf("Invalid task maintenance property, expected format: %s$ID: %s", MaintenancePropertyPrefix, property) + } + + if maintenanceParts[1] == "" { + return fmt.Errorf("Invalid task maintenance property, missing ID") + } + + tasks, err := FetchCronTasks(FetchCronTasksInput{AppName: appName}) + if err != nil { + return err + } + + maintenanceID := maintenanceParts[1] + for _, task := range tasks { + if task.ID == maintenanceID { + validProperties[property] = "" + globalProperties[property] = false + break + } + } + + if _, ok := validProperties[property]; !ok { + return fmt.Errorf("Invalid task maintenance property, no matching task ID found: %s", property) + } + } + + common.CommandPropertySet("cron", appName, property, value, validProperties, globalProperties) scheduler := common.GetAppScheduler(appName) _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "scheduler-cron-write", @@ -147,3 +196,8 @@ func CommandSet(appName string, property string, value string) error { }) return err } + +// CommandSuspend suspends a cron task +func CommandSuspend(appName string, cronID string) error { + return CommandSet(appName, fmt.Sprintf("%s%s", MaintenancePropertyPrefix, cronID), "true") +} diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index 7a2e0e030..626f11a5d 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -163,7 +163,7 @@ func TriggerSchedulerAppStatus(scheduler string, appName string) error { return nil } -// TriggerSchedulerCronWrite writes out cron entries for a given application +// TriggerSchedulerCronWrite writes out cron tasks for a given application func TriggerSchedulerCronWrite(scheduler string, appName string) error { if scheduler != "k3s" { return nil @@ -171,7 +171,7 @@ func TriggerSchedulerCronWrite(scheduler string, appName string) error { cronTasks, err := cron.FetchCronTasks(cron.FetchCronTasksInput{AppName: appName}) if err != nil { - return fmt.Errorf("Error fetching cron entries: %w", err) + return fmt.Errorf("Error fetching cron tasks: %w", err) } ctx, cancel := context.WithCancel(context.Background()) From 66a30b0547338880f6c21d45cde9fbe1e85e307e Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 21 Sep 2025 20:58:56 -0400 Subject: [PATCH 037/144] refactor: rename cron entry to cron task This naming better suits what it is we are fetching. --- plugins/scheduler-docker-local/functions.go | 4 ++-- plugins/scheduler-docker-local/triggers.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/scheduler-docker-local/functions.go b/plugins/scheduler-docker-local/functions.go index 977a8904c..4fc6a9a9c 100644 --- a/plugins/scheduler-docker-local/functions.go +++ b/plugins/scheduler-docker-local/functions.go @@ -116,7 +116,7 @@ func generateCronTasks() ([]cron.CronTask, error) { return tasks, nil } -func writeCronTasks(scheduler string) error { +func writeCronTab(scheduler string) error { // allow empty scheduler, which means all apps (used by letsencrypt) if scheduler != "docker-local" && scheduler != "" { return nil @@ -154,7 +154,7 @@ func writeCronTasks(scheduler string) error { return err } - tmpFile, err := os.CreateTemp(os.TempDir(), fmt.Sprintf("dokku-%s-%s", common.MustGetEnv("DOKKU_PID"), "WriteCronTasks")) + tmpFile, err := os.CreateTemp(os.TempDir(), fmt.Sprintf("dokku-%s-%s", common.MustGetEnv("DOKKU_PID"), "WriteCronTab")) if err != nil { return fmt.Errorf("Cannot create temporary schedule file: %v", err) } diff --git a/plugins/scheduler-docker-local/triggers.go b/plugins/scheduler-docker-local/triggers.go index 3bd78229d..321d2dc43 100644 --- a/plugins/scheduler-docker-local/triggers.go +++ b/plugins/scheduler-docker-local/triggers.go @@ -2,5 +2,5 @@ package schedulerdockerlocal // TriggerSchedulerCronWrite force updates the cron file for all apps func TriggerSchedulerCronWrite(scheduler string) error { - return writeCronTasks(scheduler) + return writeCronTab(scheduler) } From 426e6b746a207e978a89394d04ee35d1ba6bf19b Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 21 Sep 2025 21:05:24 -0400 Subject: [PATCH 038/144] chore: standardize on cron task vs any other naming --- plugins/app-json/appjson.go | 2 +- plugins/cron/cron.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/app-json/appjson.go b/plugins/app-json/appjson.go index ab80fb21f..99c856bc5 100644 --- a/plugins/app-json/appjson.go +++ b/plugins/app-json/appjson.go @@ -55,7 +55,7 @@ type CronTask struct { // Command is the command to execute Command string `json:"command"` - // Maintenance is whether or not the cron command is in maintenance mode + // Maintenance is whether or not the cron task is in maintenance mode Maintenance bool `json:"maintenance"` // Schedule is the cron schedule to execute the command on diff --git a/plugins/cron/cron.go b/plugins/cron/cron.go index de19d07b3..f21a058fa 100644 --- a/plugins/cron/cron.go +++ b/plugins/cron/cron.go @@ -56,10 +56,10 @@ type CronTask struct { // AppInMaintenance is whether the app's cron is in maintenance mode AppInMaintenance bool `json:"app-in-maintenance"` - // Maintenance is whether the cron command is in maintenance mode + // Maintenance is whether the cron task is in maintenance mode TaskInMaintenance bool `json:"task-in-maintenance"` - // Maintenance is whether the cron command is in maintenance mode + // Maintenance is whether the cron task is in maintenance mode Maintenance bool `json:"maintenance"` } From 7461b839729ef294cb849e2dafb686c61234a38e Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Fri, 7 Nov 2025 22:09:51 -0500 Subject: [PATCH 039/144] fix: use correct makefile target --- plugins/cron/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cron/Makefile b/plugins/cron/Makefile index 46513788c..93d8c9a0a 100644 --- a/plugins/cron/Makefile +++ b/plugins/cron/Makefile @@ -1,4 +1,4 @@ -SUBCOMMANDS = subcommands/list subcommands/report cron/resume subcommands/run subcommands/set subcommands/suspend +SUBCOMMANDS = subcommands/list subcommands/report subcommands/resume subcommands/run subcommands/set subcommands/suspend TRIGGERS = triggers/app-json-is-valid triggers/cron-get-property triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-delete triggers/scheduler-stop BUILD = commands subcommands triggers PLUGIN_NAME = cron From 00ce3932d060367462059f351cb4dc27a7c2771d Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 8 Nov 2025 03:23:39 -0500 Subject: [PATCH 040/144] fix: update the json output --- tests/unit/cron.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/cron.bats b/tests/unit/cron.bats index 77a22c2ea..09bb3b6b4 100644 --- a/tests/unit/cron.bats +++ b/tests/unit/cron.bats @@ -129,7 +129,7 @@ teardown() { echo "output: $output" echo "status: $status" assert_success - assert_output '[{"id":"5cruaotm4yzzpnjlsdunblj8qyjp","command":"/bin/true","global":true,"schedule":"@daily","maintenance":false}]' + assert_output '[{"id":"5cruaotm4yzzpnjlsdunblj8qyjp","command":"/bin/true","global":true,"schedule":"@daily","app-in-maintenance":false,"task-in-maintenance":false,"maintenance":false}]' run /bin/bash -c "cat /var/spool/cron/crontabs/dokku" echo "output: $output" From 2aea9993261f5ed9bdf444bfcef9ccd3f925350e Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 9 Nov 2025 01:45:07 -0500 Subject: [PATCH 041/144] test: check if uninstalling after deleting apps will cause tests to work again Not clear why they are suddenly failing, but lets switch this around for now. --- tests/unit/scheduler-k3s-2.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/scheduler-k3s-2.bats b/tests/unit/scheduler-k3s-2.bats index 978bfa531..325c0c286 100644 --- a/tests/unit/scheduler-k3s-2.bats +++ b/tests/unit/scheduler-k3s-2.bats @@ -5,8 +5,8 @@ load test_helper TEST_APP="rdmtestapp" setup() { - uninstall_k3s || true global_setup + uninstall_k3s || true dokku nginx:stop export KUBECONFIG="/etc/rancher/k3s/k3s.yaml" } From e30b579de5119654117cba8e09b2b58a10cfeafa Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 9 Nov 2025 01:51:57 -0500 Subject: [PATCH 042/144] test: fix issue where teardown was not being called --- tests/unit/scheduler-k3s-2.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/scheduler-k3s-2.bats b/tests/unit/scheduler-k3s-2.bats index 325c0c286..978bfa531 100644 --- a/tests/unit/scheduler-k3s-2.bats +++ b/tests/unit/scheduler-k3s-2.bats @@ -5,8 +5,8 @@ load test_helper TEST_APP="rdmtestapp" setup() { - global_setup uninstall_k3s || true + global_setup dokku nginx:stop export KUBECONFIG="/etc/rancher/k3s/k3s.yaml" } From d9603af08ad4b2e1fbe8483d3d02f4a5850acb51 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 10 Nov 2025 01:55:44 -0500 Subject: [PATCH 043/144] docs: document cron task suspension --- docs/processes/scheduled-cron-tasks.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/processes/scheduled-cron-tasks.md b/docs/processes/scheduled-cron-tasks.md index a8d39fdfc..39f659586 100644 --- a/docs/processes/scheduled-cron-tasks.md +++ b/docs/processes/scheduled-cron-tasks.md @@ -6,8 +6,10 @@ ``` cron:list [--format json|stdout] # List scheduled cron tasks for an app cron:report [] [] # Display report about an app +cron:resume # Resume a cron task cron:run [--detach] # Run a cron task on the fly cron:set [--global|] # Set or clear a cron property for an app +cron:suspend # Suspend a cron task ``` ## Usage @@ -124,6 +126,26 @@ ID Schedule Command 5cruaotm4yzzpnjlsdunblj8qyjp @daily /bin/true ``` +#### Suspending and resuming a specific cron task + +Cron tasks can be suspended to temporarily prevent them from running, and later resumed to re-enable them. This is useful for maintenance or debugging purposes. + +To suspend a specific cron task, use the `cron:suspend` command with the app name and cron ID: + +```shell +dokku cron:suspend node-js-app cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5 +``` + +A suspended task will not execute according to its schedule. You can verify a task is suspended by checking the `Maintenance` column in the `cron:list` output, which will show `true (task)` for suspended tasks. + +To resume a suspended cron task, use the `cron:resume` command: + +```shell +dokku cron:resume node-js-app cGhwPT09cGhwIHRlc3QucGhwPT09QGRhaWx5 +``` + +Once resumed, the task will execute according to its schedule again. The cron ID can be retrieved from the `cron:list` output. + #### Executing a cron task on the fly Cron tasks can be invoked via the `cron:run` command. This command takes an `app` argument and a `cron id` (retrievable from `cron:list` output). From 4929272c870a1dd17c40ebf0be777a6bee068407 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 10 Nov 2025 01:56:07 -0500 Subject: [PATCH 044/144] fix: calculate maintenance property correctly --- plugins/cron/cron.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/cron/cron.go b/plugins/cron/cron.go index f21a058fa..1d02ade85 100644 --- a/plugins/cron/cron.go +++ b/plugins/cron/cron.go @@ -135,8 +135,9 @@ func FetchCronTasks(input FetchCronTasksInput) ([]CronTask, error) { return tasks, fmt.Errorf("Invalid cron schedule for app %s (schedule %s): %s", appName, c.Schedule, err.Error()) } + cronID := GenerateCommandID(appName, c) maintenance := c.Maintenance - if value, ok := properties[MaintenancePropertyPrefix+c.Schedule]; ok { + if value, ok := properties[MaintenancePropertyPrefix+cronID]; ok { boolValue, err := strconv.ParseBool(value) if err != nil { return tasks, fmt.Errorf("Invalid maintenance property for app %s (schedule %s): %s", appName, c.Schedule, err.Error()) @@ -152,7 +153,7 @@ func FetchCronTasks(input FetchCronTasksInput) ([]CronTask, error) { App: appName, Command: c.Command, Schedule: c.Schedule, - ID: GenerateCommandID(appName, c), + ID: cronID, Maintenance: isAppCronInMaintenance || maintenance, AppInMaintenance: isAppCronInMaintenance, TaskInMaintenance: maintenance, From 3638399726d1a30bbe2929d044de761c2a7b5666 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 10 Nov 2025 01:56:59 -0500 Subject: [PATCH 045/144] fix: parse cronID correctly --- plugins/cron/subcommands.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/plugins/cron/subcommands.go b/plugins/cron/subcommands.go index aef1b833e..233361953 100644 --- a/plugins/cron/subcommands.go +++ b/plugins/cron/subcommands.go @@ -159,12 +159,8 @@ func CommandSet(appName string, property string, value string) error { return fmt.Errorf("Task maintenance properties cannot be set globally") } - maintenanceParts := strings.SplitN(property, ".", 2) - if len(maintenanceParts) != 3 { - return fmt.Errorf("Invalid task maintenance property, expected format: %s$ID: %s", MaintenancePropertyPrefix, property) - } - - if maintenanceParts[1] == "" { + cronTaskID := strings.TrimPrefix(property, MaintenancePropertyPrefix) + if cronTaskID == "" { return fmt.Errorf("Invalid task maintenance property, missing ID") } @@ -173,9 +169,8 @@ func CommandSet(appName string, property string, value string) error { return err } - maintenanceID := maintenanceParts[1] for _, task := range tasks { - if task.ID == maintenanceID { + if task.ID == cronTaskID { validProperties[property] = "" globalProperties[property] = false break From d49f9b89c28a601e20e3c5b8ad64fab835dd04f4 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 10 Nov 2025 01:57:12 -0500 Subject: [PATCH 046/144] chore: add commands to cron:help output --- plugins/cron/src/commands/commands.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/cron/src/commands/commands.go b/plugins/cron/src/commands/commands.go index 36761c778..88ae2d1ee 100644 --- a/plugins/cron/src/commands/commands.go +++ b/plugins/cron/src/commands/commands.go @@ -20,8 +20,10 @@ Additional commands:` helpContent = ` cron:list [--format json|stdout], List scheduled cron tasks for an app cron:report [] [], Display report about an app + cron:resume , Resume a cron task cron:run [--detach], Run a cron task on the fly - cron:set [--global|] , Set or clear a cron property for an app` + cron:set [--global|] , Set or clear a cron property for an app + cron:suspend , Suspend a cron task` ) func main() { From 379ada07d9b5a6cbc37a8a9ab71333dd7b04cdf1 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 10 Nov 2025 01:57:46 -0500 Subject: [PATCH 047/144] fix: properly fetch non-maintenance tasks when building crontab file --- plugins/scheduler-docker-local/functions.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/scheduler-docker-local/functions.go b/plugins/scheduler-docker-local/functions.go index 4fc6a9a9c..b5ac3eecc 100644 --- a/plugins/scheduler-docker-local/functions.go +++ b/plugins/scheduler-docker-local/functions.go @@ -107,9 +107,10 @@ func generateCronTasks() ([]cron.CronTask, error) { } for result := range results { - c := result - if len(c) > 0 && !c[0].Maintenance { - tasks = append(tasks, c...) + for _, task := range result { + if !task.Maintenance { + tasks = append(tasks, task) + } } } From 4cc2dc7844d770605c2547357917d37dc185d11f Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 10 Nov 2025 01:58:02 -0500 Subject: [PATCH 048/144] feat: add cron maintenance info to cron:report output --- plugins/cron/report.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/plugins/cron/report.go b/plugins/cron/report.go index acf9760cb..6577b0a9a 100644 --- a/plugins/cron/report.go +++ b/plugins/cron/report.go @@ -1,7 +1,9 @@ package cron import ( + "fmt" "strconv" + "strings" "github.com/dokku/dokku/plugins/common" ) @@ -21,6 +23,11 @@ func ReportSingleApp(appName string, format string, infoFlag string) error { "--cron-maintenance": reportMaintenance, } + extraFlags := addCronMaintenanceFlags(appName, infoFlag) + for flag, fn := range extraFlags { + flags[flag] = fn + } + flagKeys := []string{} for flagKey := range flags { flagKeys = append(flagKeys, flagKey) @@ -32,6 +39,24 @@ func ReportSingleApp(appName string, format string, infoFlag string) error { return common.ReportSingleApp("cron", appName, infoFlag, infoFlags, flagKeys, format, trimPrefix, uppercaseFirstCharacter) } +func addCronMaintenanceFlags(appName string, infoFlag string) map[string]common.ReportFunc { + flags := map[string]common.ReportFunc{} + + properties, err := common.PropertyGetAllByPrefix("cron", appName, MaintenancePropertyPrefix) + if err != nil { + return flags + } + + for property, value := range properties { + key := strings.Replace(property, MaintenancePropertyPrefix, "", 1) + flags[fmt.Sprintf("--cron-maintenance-%s", key)] = func(appName string) string { + return value + } + } + + return flags +} + func reportMailfrom(_ string) string { return common.PropertyGet("cron", "--global", "mailfrom") } From 8ba6ce94fce678f97b7ee6868d5cfb00fd62c642 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 10 Nov 2025 02:00:14 -0500 Subject: [PATCH 049/144] test: add tests for cron suspend and resume commands --- tests/unit/cron.bats | 68 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/unit/cron.bats b/tests/unit/cron.bats index 09bb3b6b4..5c0b1345d 100644 --- a/tests/unit/cron.bats +++ b/tests/unit/cron.bats @@ -235,6 +235,74 @@ teardown() { assert_output "['task.py', 'schedule', 'now']" } +@test "(cron) cron:suspend cron:resume" { + run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_multiple + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "cat /var/spool/cron/crontabs/dokku" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "python3 task.py first" + assert_output_contains "python3 task.py second" + + cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')" + run /bin/bash -c "echo $cron_id" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_exists + + first_command="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].command')" + run /bin/bash -c "echo $first_command" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_exists + + run /bin/bash -c "dokku cron:report $TEST_APP --cron-maintenance-$cron_id" + echo "output: $output" + echo "status: $status" + assert_failure + + run /bin/bash -c "dokku cron:suspend $TEST_APP $cron_id" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku cron:report $TEST_APP --cron-maintenance-$cron_id" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "true" + + run /bin/bash -c "cat /var/spool/cron/crontabs/dokku" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "python3 task.py first" 0 + assert_output_contains "python3 task.py second" + + run /bin/bash -c "dokku cron:resume $TEST_APP $cron_id" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku cron:report $TEST_APP --cron-maintenance-$cron_id" + echo "output: $output" + echo "status: $status" + assert_failure + + run /bin/bash -c "cat /var/spool/cron/crontabs/dokku" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "python3 task.py first" + assert_output_contains "python3 task.py second" +} + template_cron_file_invalid() { local APP="$1" local APP_REPO_DIR="$2" From d72c183d9e4cfa60e20656960ee5c007502d4f21 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 21 Sep 2025 18:00:07 -0400 Subject: [PATCH 050/144] feat: add ability to specify a concurrency policy for crons in the app.json file --- docs/appendices/file-formats/app-json.md | 1 + plugins/app-json/appjson.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/docs/appendices/file-formats/app-json.md b/docs/appendices/file-formats/app-json.md index c7500e19f..842674376 100644 --- a/docs/appendices/file-formats/app-json.md +++ b/docs/appendices/file-formats/app-json.md @@ -23,6 +23,7 @@ - `command`: (string, required) - `maintenance`: (boolean, optional) - `schedule`: (string, required) +- `concurrency_policy`: (string, optional, default: `allow`, options: `allow`, `deny`, `replace`) ## Formation diff --git a/plugins/app-json/appjson.go b/plugins/app-json/appjson.go index 99c856bc5..fe59c9942 100644 --- a/plugins/app-json/appjson.go +++ b/plugins/app-json/appjson.go @@ -60,6 +60,9 @@ type CronTask struct { // Schedule is the cron schedule to execute the command on Schedule string `json:"schedule"` + + // ConcurrencyPolicy is the concurrency policy for the cron command + ConcurrencyPolicy string `json:"concurrency_policy"` } // Formation is a struct that represents the scale for a process from an app.json file From f1e3c0764c1e7bb25b61b2d6ed574da2b5399afe Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 21 Sep 2025 18:06:21 -0400 Subject: [PATCH 051/144] feat: expose the concurrency policy when fetching cron entries Also add some validation to ensure invalid values are not specified. --- plugins/cron/cron.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/cron/cron.go b/plugins/cron/cron.go index 1d02ade85..aa62ed75c 100644 --- a/plugins/cron/cron.go +++ b/plugins/cron/cron.go @@ -47,6 +47,9 @@ type CronTask struct { // Schedule is the cron schedule Schedule string `json:"schedule"` + // ConcurrencyPolicy is the concurrency policy for the cron command + ConcurrencyPolicy string `json:"concurrency_policy"` + // AltCommand is an alternate command to run AltCommand string `json:"-"` @@ -148,12 +151,19 @@ func FetchCronTasks(input FetchCronTasksInput) ([]CronTask, error) { maintenance = boolValue } } + if c.ConcurrencyPolicy == "" { + c.ConcurrencyPolicy = "allow" + } + if c.ConcurrencyPolicy != "allow" && c.ConcurrencyPolicy != "forbid" && c.ConcurrencyPolicy != "replace" { + return tasks, fmt.Errorf("Invalid cron concurrency policy for app %s (schedule %s): %s", appName, c.Schedule, c.ConcurrencyPolicy) + } tasks = append(tasks, CronTask{ App: appName, Command: c.Command, Schedule: c.Schedule, ID: cronID, + ConcurrencyPolicy: c.ConcurrencyPolicy, Maintenance: isAppCronInMaintenance || maintenance, AppInMaintenance: isAppCronInMaintenance, TaskInMaintenance: maintenance, From f38bc641194ab67b22da077502a95bcae66418cf Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 21 Sep 2025 18:12:25 -0400 Subject: [PATCH 052/144] feat: expose concurrency policy to kubernetes jobs --- plugins/scheduler-k3s/template.go | 17 +++++++++++++---- .../scheduler-k3s/templates/chart/cron-job.yaml | 2 +- plugins/scheduler-k3s/triggers.go | 10 ++++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/plugins/scheduler-k3s/template.go b/plugins/scheduler-k3s/template.go index 17eafdde5..dcc6060d8 100644 --- a/plugins/scheduler-k3s/template.go +++ b/plugins/scheduler-k3s/template.go @@ -299,12 +299,21 @@ const ( ) type ProcessCron struct { - ID string `yaml:"id"` - Schedule string `yaml:"schedule"` - Suffix string `yaml:"suffix"` - Suspend bool `yaml:"suspend"` + ID string `yaml:"id"` + Schedule string `yaml:"schedule"` + Suffix string `yaml:"suffix"` + Suspend bool `yaml:"suspend"` + ConcurrencyPolicy ProcessCronConcurrencyPolicy `yaml:"concurrency_policy"` } +type ProcessCronConcurrencyPolicy string + +const ( + ProcessCronConcurrencyPolicy_Allow ProcessCronConcurrencyPolicy = "Allow" + ProcessCronConcurrencyPolicy_Forbid ProcessCronConcurrencyPolicy = "Forbid" + ProcessCronConcurrencyPolicy_Replace ProcessCronConcurrencyPolicy = "Replace" +) + type ProcessPortMap struct { ContainerPort int32 `yaml:"container_port"` HostPort int32 `yaml:"host_port"` diff --git a/plugins/scheduler-k3s/templates/chart/cron-job.yaml b/plugins/scheduler-k3s/templates/chart/cron-job.yaml index b5b4e8b88..102b3ce1e 100644 --- a/plugins/scheduler-k3s/templates/chart/cron-job.yaml +++ b/plugins/scheduler-k3s/templates/chart/cron-job.yaml @@ -26,7 +26,7 @@ metadata: name: {{ $.Values.global.app_name }}-cron-{{ $config.cron.suffix }} namespace: {{ $.Values.global.namespace }} spec: - concurrencyPolicy: Allow + concurrencyPolicy: {{ $config.cron.concurrency_policy }} failedJobsHistoryLimit: 10 jobTemplate: metadata: diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index 626f11a5d..1086a7163 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -699,14 +699,16 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e return fmt.Errorf("Error getting process labels: %w", err) } + concurrencyPolicy := strings.ToUpper(cronTask.ConcurrencyPolicy) processValues := ProcessValues{ Args: words, Annotations: annotations, Cron: ProcessCron{ - ID: cronTask.ID, - Schedule: cronTask.Schedule, - Suffix: suffix, - Suspend: cronTask.Maintenance, + ID: cronTask.ID, + Schedule: cronTask.Schedule, + Suffix: suffix, + Suspend: cronTask.Maintenance, + ConcurrencyPolicy: ProcessCronConcurrencyPolicy(concurrencyPolicy), }, Labels: labels, ProcessType: ProcessType_Cron, From 20be426125d1aa1f52a276177565a98a2eba0929 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 21 Sep 2025 19:10:01 -0400 Subject: [PATCH 053/144] feat: add support for locking in docker-local commands --- plugins/cron/cron.go | 2 +- plugins/run/internal-functions | 17 ++++++++- plugins/scheduler-docker-local/scheduler-run | 39 ++++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/plugins/cron/cron.go b/plugins/cron/cron.go index aa62ed75c..89d19f552 100644 --- a/plugins/cron/cron.go +++ b/plugins/cron/cron.go @@ -75,7 +75,7 @@ func (t CronTask) DokkuRunCommand() string { return t.AltCommand } - return fmt.Sprintf("dokku run --cron-id %s %s %s", t.ID, t.App, t.Command) + return fmt.Sprintf("dokku run --concurrency-policy %s --cron-id %s %s %s", t.ConcurrencyPolicy, t.ID, t.App, t.Command) } // FetchCronTasksInput is the input for the FetchCronTasks function diff --git a/plugins/run/internal-functions b/plugins/run/internal-functions index b4ccfc2ce..fe45b1790 100755 --- a/plugins/run/internal-functions +++ b/plugins/run/internal-functions @@ -9,7 +9,7 @@ fn-run() { shift 1 declare APP="" - local CRON_ID + local CRON_ID CONCURRENCY_POLICY="allow" declare -a RUN_ENV RUN_ENV=() while [[ $# -gt 0 ]]; do @@ -19,6 +19,11 @@ fn-run() { CRON_ID="$arg" shift ;; + --concurrency-policy=*) + local arg=$(printf "%s" "$1" | sed -E 's/(^--concurrency-policy=)//g') + CONCURRENCY_POLICY="$arg" + shift + ;; --no-tty) export DOKKU_DISABLE_TTY=true shift @@ -35,6 +40,14 @@ fn-run() { CRON_ID="$2" shift 2 ;; + --concurrency-policy) + if [[ ! $2 ]]; then + dokku_log_warn "expected $1 to have an argument" + break + fi + CONCURRENCY_POLICY="$2" + shift 2 + ;; -e=* | --env=*) local arg=$(printf "%s" "$1" | sed -E 's/(^-e=)|(^--env=)//g') RUN_ENV+=("$arg") @@ -67,7 +80,7 @@ fn-run() { verify_app_name "$APP" local DOKKU_SCHEDULER=$(get_app_scheduler "$APP") - DOKKU_CRON_ID="$CRON_ID" plugn trigger scheduler-run "$DOKKU_SCHEDULER" "$APP" "${#RUN_ENV[@]}" "${RUN_ENV[@]}" -- "$@" + DOKKU_CRON_ID="$CRON_ID" DOKKU_CONCURRENCY_POLICY="$CONCURRENCY_POLICY" plugn trigger scheduler-run "$DOKKU_SCHEDULER" "$APP" "${#RUN_ENV[@]}" "${RUN_ENV[@]}" -- "$@" } cmd-run() { diff --git a/plugins/scheduler-docker-local/scheduler-run b/plugins/scheduler-docker-local/scheduler-run index cb6bbfb74..096210dc3 100755 --- a/plugins/scheduler-docker-local/scheduler-run +++ b/plugins/scheduler-docker-local/scheduler-run @@ -5,6 +5,25 @@ source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" source "$PLUGIN_AVAILABLE_PATH/config/functions" source "$PLUGIN_AVAILABLE_PATH/scheduler-docker-local/internal-functions" +fn-scheduler-docker-local-scheduler-run-acquire-lock() { + declare desc="acquire advisory lock for use in docker-local cron jobs" + local APP="$1" CRON_ID="$2" + local LOCK_TYPE="exclusive" + local APP_DEPLOY_LOCK_FILE="$DOKKU_LIB_ROOT/data/scheduler-docker-local/$APP/cron-$CRON_ID.lock" + local LOCK_WAITING_MSG="$APP currently has a cron lock in place for $CRON_ID. Waiting..." + local LOCK_FAILED_MSG="$APP currently has a cron lock in place for $CRON_ID. Exiting..." + + acquire_advisory_lock "$APP_DEPLOY_LOCK_FILE" "$LOCK_TYPE" "$LOCK_WAITING_MSG" "$LOCK_FAILED_MSG" +} + +fn-scheduler-docker-local-scheduler-run-release-lock() { + declare desc="release advisory lock used in docker-local cron jobs" + local APP="$1" CRON_ID="$2" + local APP_DEPLOY_LOCK_FILE="$DOKKU_LIB_ROOT/data/scheduler-docker-local/$APP/cron-$CRON_ID.lock" + + release_advisory_lock "$APP_DEPLOY_LOCK_FILE" +} + trigger-scheduler-docker-local-scheduler-run() { declare desc="runs command in container based on app image" declare trigger="scheduler-run" @@ -39,9 +58,24 @@ trigger-scheduler-docker-local-scheduler-run() { [[ "$DOKKU_TRACE" ]] && DOCKER_ARGS+=" -e TRACE=true " local PROCESS_TYPE=run + local concurrency_policy="allow" if [[ -n "$DOKKU_CRON_ID" ]]; then PROCESS_TYPE=cron DOCKER_ARGS+=" --label=com.dokku.cron-id=$DOKKU_CRON_ID" + + if [[ -n "$DOKKU_CONCURRENCY_POLICY" ]]; then + concurrency_policy="$DOKKU_CONCURRENCY_POLICY" + fi + concurrency_policy="$DOKKU_CONCURRENCY_POLICY" + DOCKER_ARGS+=" --label=com.dokku.concurrency-policy=$concurrency_policy" + + if [[ "$concurrency_policy" == "forbid" ]]; then + fn-scheduler-docker-local-scheduler-run-acquire-lock "$APP" "$DOKKU_CRON_ID" + fi + + if [[ "$concurrency_policy" == "replace" ]]; then + dokku_log_warn "Concurrency policy 'replace' is not supported for docker-local cron jobs" + fi fi local DYNO_NUMBER="$RANDOM" @@ -138,6 +172,11 @@ trigger-scheduler-docker-local-scheduler-run() { DOKKU_CONTAINER_EXIT_CODE="$("$DOCKER_BIN" container wait "$CONTAINER_ID" 2>/dev/null || echo "$EXIT_CODE")" [[ -z "$DOKKU_CONTAINER_EXIT_CODE" ]] && DOKKU_CONTAINER_EXIT_CODE=0 fi + + if [[ -n "$DOKKU_CRON_ID" ]] && [[ "$concurrency_policy" == "forbid" ]]; then + fn-scheduler-docker-local-scheduler-run-release-lock "$APP" "$DOKKU_CRON_ID" + fi + plugn trigger scheduler-post-run "$DOKKU_SCHEDULER" "$APP" "$CONTAINER_ID" "$DOKKU_CONTAINER_EXIT_CODE" return "$DOKKU_CONTAINER_EXIT_CODE" } From b8f32f6ea42d9b71dba53e1dce32563976f19e8e Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 5 Oct 2025 15:45:47 -0400 Subject: [PATCH 054/144] tests: add concurrency_policy to test check --- tests/unit/cron.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/cron.bats b/tests/unit/cron.bats index 5c0b1345d..671e8f58f 100644 --- a/tests/unit/cron.bats +++ b/tests/unit/cron.bats @@ -129,7 +129,7 @@ teardown() { echo "output: $output" echo "status: $status" assert_success - assert_output '[{"id":"5cruaotm4yzzpnjlsdunblj8qyjp","command":"/bin/true","global":true,"schedule":"@daily","app-in-maintenance":false,"task-in-maintenance":false,"maintenance":false}]' + assert_output '[{"id":"5cruaotm4yzzpnjlsdunblj8qyjp","command":"/bin/true","global":true,"schedule":"@daily","concurrency_policy":"","app-in-maintenance":false,"task-in-maintenance":false,"maintenance":false}]' run /bin/bash -c "cat /var/spool/cron/crontabs/dokku" echo "output: $output" From 983115c8277979b7045aefa74b122369f57e914c Mon Sep 17 00:00:00 2001 From: Corey Taylor Date: Tue, 21 Oct 2025 21:44:11 -0500 Subject: [PATCH 055/144] Add test for cron concurrency policy --- docs/appendices/file-formats/app-json.md | 2 +- plugins/cron/subcommands.go | 4 +- plugins/scheduler-docker-local/scheduler-run | 1 - tests/unit/cron.bats | 42 ++++++++++++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/docs/appendices/file-formats/app-json.md b/docs/appendices/file-formats/app-json.md index 842674376..b3cd72c7b 100644 --- a/docs/appendices/file-formats/app-json.md +++ b/docs/appendices/file-formats/app-json.md @@ -23,7 +23,7 @@ - `command`: (string, required) - `maintenance`: (boolean, optional) - `schedule`: (string, required) -- `concurrency_policy`: (string, optional, default: `allow`, options: `allow`, `deny`, `replace`) +- `concurrency_policy`: (string, optional, default: `allow`, options: `allow`, `forbid`, `replace`) ## Formation diff --git a/plugins/cron/subcommands.go b/plugins/cron/subcommands.go index 233361953..c47fa3289 100644 --- a/plugins/cron/subcommands.go +++ b/plugins/cron/subcommands.go @@ -42,7 +42,7 @@ func CommandList(appName string, format string) error { } if format == "stdout" { - output := []string{"ID | Schedule | Maintenance | Command"} + output := []string{"ID | Schedule | Concurrency | Maintenance | Command"} for _, task := range tasks { maintenance := "false" if task.Maintenance { @@ -52,7 +52,7 @@ func CommandList(appName string, format string) error { maintenance = "true (app)" } } - output = append(output, fmt.Sprintf("%s | %s | %s | %s", task.ID, task.Schedule, maintenance, task.Command)) + output = append(output, fmt.Sprintf("%s | %s | %s | %t | %s", task.ID, task.Schedule, task.ConcurrencyPolicy, maintenance, task.Command)) } result := columnize.SimpleFormat(output) diff --git a/plugins/scheduler-docker-local/scheduler-run b/plugins/scheduler-docker-local/scheduler-run index 096210dc3..0df116077 100755 --- a/plugins/scheduler-docker-local/scheduler-run +++ b/plugins/scheduler-docker-local/scheduler-run @@ -66,7 +66,6 @@ trigger-scheduler-docker-local-scheduler-run() { if [[ -n "$DOKKU_CONCURRENCY_POLICY" ]]; then concurrency_policy="$DOKKU_CONCURRENCY_POLICY" fi - concurrency_policy="$DOKKU_CONCURRENCY_POLICY" DOCKER_ARGS+=" --label=com.dokku.concurrency-policy=$concurrency_policy" if [[ "$concurrency_policy" == "forbid" ]]; then diff --git a/tests/unit/cron.bats b/tests/unit/cron.bats index 671e8f58f..fa5d8a0e4 100644 --- a/tests/unit/cron.bats +++ b/tests/unit/cron.bats @@ -235,6 +235,30 @@ teardown() { assert_output "['task.py', 'schedule', 'now']" } +@test "(cron) cron:run concurrency_policy forbid" { + run deploy_app dockerfile dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_concurrency_forbid + echo "output: $output" + echo "status: $status" + assert_success + + cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')" + run /bin/bash -c "echo cron $cron_id" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_exists + + run /bin/bash -c "dokku cron:run $TEST_APP $cron_id --detach" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku cron:run $TEST_APP $cron_id" + echo "output: $output" + echo "status: $status" + assert_failure +} + @test "(cron) cron:suspend cron:resume" { run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_cron_file_valid_multiple echo "output: $output" @@ -427,3 +451,21 @@ template_cron_file_valid_multiple() { } EOF } + +template_cron_file_concurrency_forbid() { + local APP="$1" + local APP_REPO_DIR="$2" + [[ -z "$APP" ]] && local APP="$TEST_APP" + echo "injecting valid cron app.json -> $APP_REPO_DIR/app.json" + cat <"$APP_REPO_DIR/app.json" +{ + "cron": [ + { + "command": "sleep 5", + "schedule": "0 0 * * *", + "concurrency_policy": "forbid" + } + ] +} +EOF +} From c95cb526bed17b45c71a16a6d4db345c4d864054 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 27 Oct 2025 14:28:18 -0400 Subject: [PATCH 056/144] fix: correct lint issue --- tests/unit/cron.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/cron.bats b/tests/unit/cron.bats index fa5d8a0e4..6da29182d 100644 --- a/tests/unit/cron.bats +++ b/tests/unit/cron.bats @@ -240,7 +240,7 @@ teardown() { echo "output: $output" echo "status: $status" assert_success - + cron_id="$(dokku cron:list $TEST_APP --format json | jq -r '.[0].id')" run /bin/bash -c "echo cron $cron_id" echo "output: $output" From 8c2731b328683a7492a4aefbeab7a90679d97064 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 27 Oct 2025 14:37:13 -0400 Subject: [PATCH 057/144] fix: properly set concurrency policy for golang --- plugins/scheduler-k3s/triggers.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index 1086a7163..7ae406ee4 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -700,6 +700,16 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e } concurrencyPolicy := strings.ToUpper(cronTask.ConcurrencyPolicy) + switch concurrencyPolicy { + case "ALLOW": + concurrencyPolicy = "Allow" + case "FORBID": + concurrencyPolicy = "Forbid" + case "REPLACE": + concurrencyPolicy = "Replace" + default: + return fmt.Errorf("Invalid concurrency_policy specified: %v", concurrencyPolicy) + } processValues := ProcessValues{ Args: words, Annotations: annotations, From 3162fa5b9a64f342c95aa11f39da477d345eed94 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Mon, 10 Nov 2025 18:02:46 -0500 Subject: [PATCH 058/144] debug: run sleep for longer and check the inspect output of the cron container --- tests/unit/cron.bats | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/unit/cron.bats b/tests/unit/cron.bats index 6da29182d..8b37cac2c 100644 --- a/tests/unit/cron.bats +++ b/tests/unit/cron.bats @@ -253,6 +253,12 @@ teardown() { echo "status: $status" assert_success + run /bin/bash -c "docker ps --filter "label=com.dokku.cron-id=$cron_id" -q | xargs docker inspect" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_exists + run /bin/bash -c "dokku cron:run $TEST_APP $cron_id" echo "output: $output" echo "status: $status" @@ -461,7 +467,7 @@ template_cron_file_concurrency_forbid() { { "cron": [ { - "command": "sleep 5", + "command": "sleep 30", "schedule": "0 0 * * *", "concurrency_policy": "forbid" } From c56110b3a72ad600c421af701c3c17ff4938108f Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 13 Nov 2025 01:52:08 -0500 Subject: [PATCH 059/144] fix: do not print errors twice --- plugins/common/log.go | 4 +++- plugins/cron/subcommands.go | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/common/log.go b/plugins/common/log.go index 9a3281138..9e67287b4 100644 --- a/plugins/common/log.go +++ b/plugins/common/log.go @@ -90,7 +90,9 @@ func LogFailWithError(err error) { fmt.Fprintf(os.Stderr, " ! %s\n", e.Error()) } } else { - fmt.Fprintf(os.Stderr, " ! %s\n", err.Error()) + if err.Error() != "" { + fmt.Fprintf(os.Stderr, " ! %s\n", err.Error()) + } } if errExit, ok := err.(ErrWithExitCode); ok { os.Exit(errExit.ExitCode()) diff --git a/plugins/cron/subcommands.go b/plugins/cron/subcommands.go index c47fa3289..c46ba4771 100644 --- a/plugins/cron/subcommands.go +++ b/plugins/cron/subcommands.go @@ -143,6 +143,11 @@ func CommandRun(appName string, cronID string, detached bool) error { Args: args, StreamStdio: true, }) + if err != nil { + // return an error with an empty message to avoid + // printing the error message twice + return errors.New("") + } return err } From 3f8c868ab856eeee82c1e9d433694e3f205c45cb Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 13 Nov 2025 01:53:33 -0500 Subject: [PATCH 060/144] fix: ensure the concurrency policy is propagated when running a cron command directly --- plugins/cron/subcommands.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/cron/subcommands.go b/plugins/cron/subcommands.go index c46ba4771..8bc86677f 100644 --- a/plugins/cron/subcommands.go +++ b/plugins/cron/subcommands.go @@ -112,9 +112,11 @@ func CommandRun(appName string, cronID string, detached bool) error { } command := "" + concurrencyPolicy := "allow" for _, task := range tasks { if task.ID == cronID { command = task.Command + concurrencyPolicy = task.ConcurrencyPolicy } } @@ -134,6 +136,7 @@ func CommandRun(appName string, cronID string, detached bool) error { os.Setenv("DOKKU_DISABLE_TTY", "true") } + os.Setenv("DOKKU_CONCURRENCY_POLICY", concurrencyPolicy) os.Setenv("DOKKU_CRON_ID", cronID) os.Setenv("DOKKU_RM_CONTAINER", "1") scheduler := common.GetAppScheduler(appName) From bacbc6d90d4c2055235f5a4e2fbaef3ab0de22b2 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 13 Nov 2025 01:54:47 -0500 Subject: [PATCH 061/144] fix: correctly check for running containers when concurrency policy is forbid We cannot grab a lock as cron:run with --detach will release the lock. Instead, just check if any containers for the specified cron_id are running and exit. --- plugins/scheduler-docker-local/scheduler-run | 30 ++++---------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/plugins/scheduler-docker-local/scheduler-run b/plugins/scheduler-docker-local/scheduler-run index 0df116077..3ff3077b6 100755 --- a/plugins/scheduler-docker-local/scheduler-run +++ b/plugins/scheduler-docker-local/scheduler-run @@ -5,25 +5,6 @@ source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" source "$PLUGIN_AVAILABLE_PATH/config/functions" source "$PLUGIN_AVAILABLE_PATH/scheduler-docker-local/internal-functions" -fn-scheduler-docker-local-scheduler-run-acquire-lock() { - declare desc="acquire advisory lock for use in docker-local cron jobs" - local APP="$1" CRON_ID="$2" - local LOCK_TYPE="exclusive" - local APP_DEPLOY_LOCK_FILE="$DOKKU_LIB_ROOT/data/scheduler-docker-local/$APP/cron-$CRON_ID.lock" - local LOCK_WAITING_MSG="$APP currently has a cron lock in place for $CRON_ID. Waiting..." - local LOCK_FAILED_MSG="$APP currently has a cron lock in place for $CRON_ID. Exiting..." - - acquire_advisory_lock "$APP_DEPLOY_LOCK_FILE" "$LOCK_TYPE" "$LOCK_WAITING_MSG" "$LOCK_FAILED_MSG" -} - -fn-scheduler-docker-local-scheduler-run-release-lock() { - declare desc="release advisory lock used in docker-local cron jobs" - local APP="$1" CRON_ID="$2" - local APP_DEPLOY_LOCK_FILE="$DOKKU_LIB_ROOT/data/scheduler-docker-local/$APP/cron-$CRON_ID.lock" - - release_advisory_lock "$APP_DEPLOY_LOCK_FILE" -} - trigger-scheduler-docker-local-scheduler-run() { declare desc="runs command in container based on app image" declare trigger="scheduler-run" @@ -69,7 +50,12 @@ trigger-scheduler-docker-local-scheduler-run() { DOCKER_ARGS+=" --label=com.dokku.concurrency-policy=$concurrency_policy" if [[ "$concurrency_policy" == "forbid" ]]; then - fn-scheduler-docker-local-scheduler-run-acquire-lock "$APP" "$DOKKU_CRON_ID" + # check if there is a running docker container with the same com.dokku.cron-id label + local RUNNING_CONTAINERS="$("$DOCKER_BIN" container ls --filter "label=com.dokku.cron-id=$DOKKU_CRON_ID" --filter "status=running" --quiet || true)" + if [[ -n "$RUNNING_CONTAINERS" ]]; then + dokku_log_warn "$APP currently has a cron lock in place for $DOKKU_CRON_ID. Exiting..." + return 1 + fi fi if [[ "$concurrency_policy" == "replace" ]]; then @@ -172,10 +158,6 @@ trigger-scheduler-docker-local-scheduler-run() { [[ -z "$DOKKU_CONTAINER_EXIT_CODE" ]] && DOKKU_CONTAINER_EXIT_CODE=0 fi - if [[ -n "$DOKKU_CRON_ID" ]] && [[ "$concurrency_policy" == "forbid" ]]; then - fn-scheduler-docker-local-scheduler-run-release-lock "$APP" "$DOKKU_CRON_ID" - fi - plugn trigger scheduler-post-run "$DOKKU_SCHEDULER" "$APP" "$CONTAINER_ID" "$DOKKU_CONTAINER_EXIT_CODE" return "$DOKKU_CONTAINER_EXIT_CODE" } From 42a92eb6549f4ee5d6520fb6a3f3a8c6e7b2e86d Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 13 Nov 2025 01:55:12 -0500 Subject: [PATCH 062/144] feat: add support for replace concurrency policy --- plugins/scheduler-docker-local/scheduler-run | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/scheduler-docker-local/scheduler-run b/plugins/scheduler-docker-local/scheduler-run index 3ff3077b6..b83510684 100755 --- a/plugins/scheduler-docker-local/scheduler-run +++ b/plugins/scheduler-docker-local/scheduler-run @@ -59,7 +59,13 @@ trigger-scheduler-docker-local-scheduler-run() { fi if [[ "$concurrency_policy" == "replace" ]]; then - dokku_log_warn "Concurrency policy 'replace' is not supported for docker-local cron jobs" + local RUNNING_CONTAINERS="$("$DOCKER_BIN" container ls --filter "label=com.dokku.cron-id=$DOKKU_CRON_ID" --filter "status=running" --quiet || true)" + if [[ -n "$RUNNING_CONTAINERS" ]]; then + dokku_log_warn "Replacing running container for $DOKKU_CRON_ID" + "$DOCKER_BIN" container update --restart=no "$RUNNING_CONTAINERS" &>/dev/null || true + "$DOCKER_BIN" container stop "$RUNNING_CONTAINERS" >/dev/null && "$DOCKER_BIN" container kill "$RUNNING_CONTAINERS" &>/dev/null + plugn trigger scheduler-register-retired "$APP" "$RUNNING_CONTAINERS" + fi fi fi From c6bd80aca68169555d412f37086d678ba4b67e5c Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 13 Nov 2025 01:55:32 -0500 Subject: [PATCH 063/144] tests: update tests --- tests/unit/cron.bats | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/cron.bats b/tests/unit/cron.bats index 8b37cac2c..5eb2bc34a 100644 --- a/tests/unit/cron.bats +++ b/tests/unit/cron.bats @@ -253,15 +253,16 @@ teardown() { echo "status: $status" assert_success - run /bin/bash -c "docker ps --filter "label=com.dokku.cron-id=$cron_id" -q | xargs docker inspect" + run /bin/bash -c "docker ps --filter "label=com.dokku.cron-id=$cron_id" -q | xargs docker inspect -f '{{ index .Config.Labels \"com.dokku.concurrency-policy\" }}'" echo "output: $output" echo "status: $status" assert_success - assert_output_exists + assert_output "forbid" run /bin/bash -c "dokku cron:run $TEST_APP $cron_id" echo "output: $output" echo "status: $status" + assert_output_contains "currently has a cron lock in place for $cron_id" assert_failure } From dc8424d8a52b911c4d87746cb90a13286a7ebd24 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 13 Nov 2025 01:58:37 -0500 Subject: [PATCH 064/144] docs: document the concurrency_policy --- docs/processes/scheduled-cron-tasks.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/processes/scheduled-cron-tasks.md b/docs/processes/scheduled-cron-tasks.md index 39f659586..6ce3b5f71 100644 --- a/docs/processes/scheduled-cron-tasks.md +++ b/docs/processes/scheduled-cron-tasks.md @@ -38,6 +38,8 @@ A cron task takes the following properties: - `command`: A command to be run within the built app image. Specified commands can also be `Procfile` entries. - `maintenance`: A boolean value that decides whether the cron task is in maintenance and therefore executable or not. - `schedule`: A [cron-compatible](https://en.wikipedia.org/wiki/Cron#Overview) scheduling definition upon which to run the command. Seconds are generally not supported. +- `concurrency_policy`: A string (default: `allow`), that controls whether the cron task can be run concurrently with another invocation of itself. Valid options are `allow` (allow concurrency), `forbid` (exit the new cron task if there is an existing one), `replace` (delete any existing cron task and start the new one). + Zero or more cron tasks can be specified per app. Cron tasks are validated after the build artifact is created but before the app is deployed, and the cron schedule is updated during the post-deploy phase. From d5b886b2b58706fd11c71b2e0da02a73d474a5d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:09:58 +0000 Subject: [PATCH 065/144] chore(deps): bump timberio/vector in /plugins/logs Bumps timberio/vector from 0.51.0-debian to 0.51.1-debian. --- updated-dependencies: - dependency-name: timberio/vector dependency-version: 0.51.1-debian dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- plugins/logs/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/logs/Dockerfile b/plugins/logs/Dockerfile index 35920604a..a5780abbb 100644 --- a/plugins/logs/Dockerfile +++ b/plugins/logs/Dockerfile @@ -1 +1 @@ -FROM timberio/vector:0.51.0-debian +FROM timberio/vector:0.51.1-debian From ebfcf24e53f2fce79e93a5b52c68ba5951082a26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:10:02 +0000 Subject: [PATCH 066/144] chore(deps): bump traefik from 3.5.4 to 3.6.1 in /plugins/traefik-vhosts Bumps traefik from 3.5.4 to 3.6.1. --- updated-dependencies: - dependency-name: traefik dependency-version: 3.6.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- plugins/traefik-vhosts/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/traefik-vhosts/Dockerfile b/plugins/traefik-vhosts/Dockerfile index 573541f50..199aa0ee8 100644 --- a/plugins/traefik-vhosts/Dockerfile +++ b/plugins/traefik-vhosts/Dockerfile @@ -1 +1 @@ -FROM traefik:3.5.4 +FROM traefik:3.6.1 From 00411decf57745023c73bf4491a7bf3f1e5b5d21 Mon Sep 17 00:00:00 2001 From: Corey Taylor Date: Sat, 15 Nov 2025 02:04:43 -0600 Subject: [PATCH 067/144] Add process-type label for run with procfile cmd --- plugins/scheduler-docker-local/scheduler-run | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scheduler-docker-local/scheduler-run b/plugins/scheduler-docker-local/scheduler-run index cb6bbfb74..286598c07 100755 --- a/plugins/scheduler-docker-local/scheduler-run +++ b/plugins/scheduler-docker-local/scheduler-run @@ -83,6 +83,7 @@ trigger-scheduler-docker-local-scheduler-run() { if [[ -n "$PROC_CMD" ]]; then dokku_log_info1_quiet "Found '$POTENTIAL_PROCFILE_KEY' in Procfile, running that command" + DOCKER_ARGS+=" --label=com.dokku.process-type=$POTENTIAL_PROCFILE_KEY" set -- "$PROC_CMD" "${@:2}" fi fi From 3f707284515088b77ee34bf7cff7afb8510ebe4c Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 16:19:15 -0500 Subject: [PATCH 068/144] feat: implement forbid/replace logic for k3s pods spawned via cron:run --- plugins/scheduler-k3s/k8s.go | 45 +++++++++++++++++++++++++++++++ plugins/scheduler-k3s/triggers.go | 44 +++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/plugins/scheduler-k3s/k8s.go b/plugins/scheduler-k3s/k8s.go index aa5a07543..14ee6002b 100644 --- a/plugins/scheduler-k3s/k8s.go +++ b/plugins/scheduler-k3s/k8s.go @@ -337,6 +337,51 @@ func (k KubernetesClient) DeleteNode(ctx context.Context, input DeleteNodeInput) return k.Client.CoreV1().Nodes().Delete(ctx, input.Name, metav1.DeleteOptions{}) } +// DeletePodInput contains all the information needed to delete a Kubernetes pod +type DeletePodInput struct { + // Name is the Kubernetes pod name + Name string + + // Namespace is the Kubernetes namespace + Namespace string + + // LabelSelector is the Kubernetes label selector + LabelSelector string +} + +// DeletePod deletes a Kubernetes pod +func (k KubernetesClient) DeletePod(ctx context.Context, input DeletePodInput) error { + if input.Name == "" && input.LabelSelector == "" { + return fmt.Errorf("name or label selector is required") + } + + if input.Name != "" && input.LabelSelector != "" { + return fmt.Errorf("name and label selector cannot be used together") + } + + if input.Name != "" { + return k.Client.CoreV1().Pods(input.Namespace).Delete(ctx, input.Name, metav1.DeleteOptions{}) + } + + // get the pods with the label selector + pods, err := k.ListPods(ctx, ListPodsInput{ + Namespace: input.Namespace, + LabelSelector: input.LabelSelector, + }) + if err != nil { + return fmt.Errorf("Error listing pods: %w", err) + } + + for _, pod := range pods { + err := k.Client.CoreV1().Pods(input.Namespace).Delete(ctx, pod.Name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("Error deleting pod %s: %w", pod.Name, err) + } + } + + return nil +} + // DeleteSecretInput contains all the information needed to delete a Kubernetes secret type DeleteSecretInput struct { // Name is the Kubernetes secret name diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index 7ae406ee4..de2e9c2c9 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -1212,10 +1212,44 @@ func TriggerSchedulerRun(scheduler string, appName string, envCount int, args [] extraEnv["TRACE"] = "true" } + namespace := getComputedNamespace(appName) + clientset, err := NewKubernetesClient() + if err != nil { + return fmt.Errorf("Error creating kubernetes client: %w", err) + } + + if err := clientset.Ping(); err != nil { + return fmt.Errorf("kubernetes api not available: %w", err) + } + processType := "run" if os.Getenv("DOKKU_CRON_ID") != "" { processType = "cron" labels["dokku.com/cron-id"] = os.Getenv("DOKKU_CRON_ID") + concurrencyPolicy := strings.ToUpper(os.Getenv("DOKKU_CONCURRENCY_POLICY")) + switch concurrencyPolicy { + case "forbid": + // check if there is a running pod with the same dokku.com/cron-id label + pods, err := clientset.ListPods(context.Background(), ListPodsInput{ + Namespace: namespace, + LabelSelector: fmt.Sprintf("dokku.com/cron-id=%s", os.Getenv("DOKKU_CRON_ID")), + }) + if err != nil { + return fmt.Errorf("Error listing pods: %w", err) + } + if len(pods) > 0 { + return fmt.Errorf("There is a running pod with the same dokku.com/cron-id label") + } + case "replace": + // delete any existing pod with the same dokku.com/cron-id label + err := clientset.DeletePod(context.Background(), DeletePodInput{ + Namespace: namespace, + LabelSelector: fmt.Sprintf("dokku.com/cron-id=%s", os.Getenv("DOKKU_CRON_ID")), + }) + if err != nil { + return fmt.Errorf("Error deleting pod: %w", err) + } + } } imageSourceType, err := common.DockerInspect(image, "{{ index .Config.Labels \"com.dokku.builder-type\" }}") @@ -1245,7 +1279,6 @@ func TriggerSchedulerRun(scheduler string, appName string, envCount int, args [] entrypoint = "/exec" } - namespace := getComputedNamespace(appName) helmAgent, err := NewHelmAgent(namespace, DevNullPrinter) if err != nil { return fmt.Errorf("Error creating helm agent: %w", err) @@ -1275,15 +1308,6 @@ func TriggerSchedulerRun(scheduler string, appName string, envCount int, args [] attachToPod := os.Getenv("DOKKU_DETACH_CONTAINER") != "1" - clientset, err := NewKubernetesClient() - if err != nil { - return fmt.Errorf("Error creating kubernetes client: %w", err) - } - - if err := clientset.Ping(); err != nil { - return fmt.Errorf("kubernetes api not available: %w", err) - } - imagePullSecrets := getComputedImagePullSecrets(appName) if imagePullSecrets == "" { imagePullSecrets = fmt.Sprintf("ims-%s.%d", appName, deploymentID) From 9aa471c1fdb881373366d37f1cb4b5cf74785f01 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 17 Sep 2025 01:45:56 -0400 Subject: [PATCH 069/144] feat: add support for specifying CNB buildpacks via buildpacks command Closes #7325 --- .../builders/buildpack-management.md | 174 ++++++++++++++++ .../builders/herokuish-buildpacks.md | 194 +++--------------- plugins/builder-pack/builder-build | 14 ++ 3 files changed, 217 insertions(+), 165 deletions(-) create mode 100644 docs/deployment/builders/buildpack-management.md diff --git a/docs/deployment/builders/buildpack-management.md b/docs/deployment/builders/buildpack-management.md new file mode 100644 index 000000000..c577e26c9 --- /dev/null +++ b/docs/deployment/builders/buildpack-management.md @@ -0,0 +1,174 @@ +# Buildpacks Management + +> [!NOTE] +> Buildpacks commands apply to both herokuish and CNB-based builds + +``` +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 +``` + +## 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 +``` + +### 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. + +By default, builders will pin their vendored buildpacks, resulting in a consistent build result for your application. 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 buildpacks:set node-js-app https://github.com/heroku/heroku-buildpack-nodejs +``` + +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 buildpacks:set node-js-app https://github.com/heroku/heroku-buildpack-nodejs#v87 +``` + +### Displaying buildpack reports for 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 computed stack: gliderlabs/herokuish:v0.7.0-22 + Buildpacks global stack: gliderlabs/herokuish:latest-24 + Buildpacks list: https://github.com/heroku/heroku-buildpack-nodejs.git + Buildpacks stack: gliderlabs/herokuish:v0.7.0-20 +=====> python-sample buildpacks information + Buildpacks computed stack: gliderlabs/herokuish:latest-24 + Buildpacks global stack: gliderlabs/herokuish:latest-24 + Buildpacks list: https://github.com/heroku/heroku-buildpack-nodejs.git,https://github.com/heroku/heroku-buildpack-python.git + Buildpacks stack: +=====> ruby-sample buildpacks information + Buildpacks computed stack: gliderlabs/herokuish:latest-24 + Buildpacks global stack: gliderlabs/herokuish:latest-24 + Buildpacks list: + Buildpacks stack: +``` + +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 +``` diff --git a/docs/deployment/builders/herokuish-buildpacks.md b/docs/deployment/builders/herokuish-buildpacks.md index f35b5e687..dfaedfbd2 100644 --- a/docs/deployment/builders/herokuish-buildpacks.md +++ b/docs/deployment/builders/herokuish-buildpacks.md @@ -4,12 +4,6 @@ > 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 buildpacks:set-property [--global|] # Set or clear a buildpacks property for an app ``` @@ -49,104 +43,6 @@ dokku builder:set node-js-app selected herokuish > Dokku will only select the `dockerfile` builder if both the `herokuish` and `pack` builders are not detected and a Dockerfile exists. See the [dockerfile builder documentation](/docs/deployment/builders/dockerfiles.md) for more information on how that builder functions. -### 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 -``` - ### Customizing the Buildpack stack builder > [!IMPORTANT] @@ -207,49 +103,6 @@ The default value may be set by passing an empty value for the option. dokku builder-herokuish:set --global allowed ``` -### Displaying buildpack reports for 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 computed stack: gliderlabs/herokuish:v0.7.0-22 - Buildpacks global stack: gliderlabs/herokuish:latest-24 - Buildpacks list: https://github.com/heroku/heroku-buildpack-nodejs.git - Buildpacks stack: gliderlabs/herokuish:v0.7.0-20 -=====> python-sample buildpacks information - Buildpacks computed stack: gliderlabs/herokuish:latest-24 - Buildpacks global stack: gliderlabs/herokuish:latest-24 - Buildpacks list: https://github.com/heroku/heroku-buildpack-nodejs.git,https://github.com/heroku/heroku-buildpack-python.git - Buildpacks stack: -=====> ruby-sample buildpacks information - Buildpacks computed stack: gliderlabs/herokuish:latest-24 - Buildpacks global stack: gliderlabs/herokuish:latest-24 - Buildpacks list: - Buildpacks stack: -``` - -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 -``` - ### Displaying builder-herokuish reports for an app > [!IMPORTANT] @@ -309,24 +162,6 @@ If an application was previously deployed via Dockerfile, the following commands dokku ports:clear node-js-app ``` -### 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. - -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 buildpacks:set node-js-app https://github.com/heroku/heroku-buildpack-nodejs -``` - -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 buildpacks:set node-js-app https://github.com/heroku/heroku-buildpack-nodejs#v87 -``` - ### `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. @@ -345,3 +180,32 @@ See the [repository management documentation](/docs/advanced-usage/repository-ma ### Specifying commands via Procfile See the [Procfile documentation](/docs/processes/process-management.md#procfile) for more information on how to specify different processes for your app. + +### Listing Buildpacks in Use + +See the [buildpack management documentation](/docs/processes/process-management.md#listing-buildpacks-in-use) for more information on how to list buildpacks in use. + +### Adding custom buildpacks + +See the [buildpack management documentation](/docs/processes/process-management.md#adding-custom-buildpacks) for more information on how to add custom buildpacks. + +### Overwriting a buildpack position + +See the [buildpack management documentation](/docs/processes/process-management.md#overwriting-a-buildpack-position) for more information on how to overwrite a buildpack position. + +### Removing a buildpack + +See the [buildpack management documentation](/docs/processes/process-management.md#removing-a-buildpack) for more information on how to remove a buildpack. + +### Clearing all buildpacks + +See the [buildpack management documentation](/docs/processes/process-management.md#clearing-all-buildpacks) for more information on how to clear all buildpacks. + +### Using a specific buildpack version + +See the [buildpack management documentation](/docs/processes/process-management.md#using-a-specific-buildpack-version) for more information on how to using a specific buildpack version + + +### Displaying buildpack reports for an app + +See the [buildpack management documentation](/docs/processes/process-management.md#displaying-buildpack-reports-for-an-app) for more information on how to display buildpack reports for an app. \ No newline at end of file diff --git a/plugins/builder-pack/builder-build b/plugins/builder-pack/builder-build index 627662202..192ebb33c 100755 --- a/plugins/builder-pack/builder-build +++ b/plugins/builder-pack/builder-build @@ -65,6 +65,20 @@ trigger-builder-pack-builder-build() { esac done + if [ -z "$BUILDPACK_URL" ]; then + PACK_ARGS+=("--builder") + PACK_ARGS+=("$BUILDPACK_URL") + elif [ -f "$SOURCECODE_WORK_DIR/.buildpacks" ]; then + while read -r buildpack; do + if [ -z "$buildpack" ]; then + continue + fi + + PACK_ARGS+=("--builder") + PACK_ARGS+=("$buildpack") + done < "$SOURCECODE_WORK_DIR/.buildpacks" + fi + pack build "$IMAGE" --builder "$DOKKU_CNB_BUILDER" --path "$SOURCECODE_WORK_DIR" --default-process web "${PACK_ARGS[@]}" "${ENV_ARGS[@]}" docker-image-labeler relabel --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=pack --label=com.dokku.app-name=$APP "$IMAGE" From 97714e9c22954cf10b0f69910b9d15a0e6a31980 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 20:28:13 -0500 Subject: [PATCH 070/144] feat: filter out unsupported build arguments with pack While this will require future updates if pack supports new build arguments, it also ensures builds safely consume arguments from other plugins. --- plugins/builder-pack/builder-build | 123 ++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 3 deletions(-) diff --git a/plugins/builder-pack/builder-build b/plugins/builder-pack/builder-build index 627662202..7eb612280 100755 --- a/plugins/builder-pack/builder-build +++ b/plugins/builder-pack/builder-build @@ -50,17 +50,134 @@ trigger-builder-pack-builder-build() { declare -a PACK_ARGS while true; do case "$1" in - --volume=* | -v=*) + -B | --builder) + PACK_ARGS+=("--builder") + PACK_ARGS+=("$2") + shift 2 + ;; + -b | --buildpack) + PACK_ARGS+=("--buildpack") + PACK_ARGS+=("$2") + shift 2 + ;; + -r | --buildpack-registry) + PACK_ARGS+=("--buildpack-registry") + PACK_ARGS+=("$2") + shift 2 + ;; + -D | --default-process) + PACK_ARGS+=("--default-process") + PACK_ARGS+=("$2") + shift 2 + ;; + -d | --descriptor) + PACK_ARGS+=("--descriptor") + PACK_ARGS+=("$2") + shift 2 + ;; + -e | --env) + PACK_ARGS+=("--env") + PACK_ARGS+=("$2") + shift 2 + ;; + -p | --path) + PACK_ARGS+=("--path") + PACK_ARGS+=("$2") + shift 2 + ;; + -t | --tag) + PACK_ARGS+=("--tag") + PACK_ARGS+=("$2") + shift 2 + ;; + --cache | --cache-image | --creation-time | --docker-host | --env-file | --extension | --gid | --insecure-registry | --lifecycle-image | --network | --platform | --post-buildpack | --pre-buildpack | --previous-image | --pull-policy | --report-output-dir | --run-image | --sbom-output-dir | --uid | --workspace) PACK_ARGS+=("$1") - shift + PACK_ARGS+=("$2") + shift 2 + ;; + --clear-cache | --disable-system-buildpacks | --publish | --trust-builder | --trust-extra-buildpacks | --userns-host) + PACK_ARGS+=("$1") + shift 1 ;; --volume | -v) PACK_ARGS+=("--volume") PACK_ARGS+=("$2") shift 2 ;; + -B=* | --builder=*) + if [[ "$1" == "--builder=*" ]]; then + PACK_ARGS+=("--builder" "${1#--builder=}") + elif [[ "$1" == "-B=*" ]]; then + PACK_ARGS+=("--builder" "${1#-B=}") + fi + shift 1 + ;; + -b=* | --buildpack=*) + if [[ "$1" == "--buildpack=*" ]]; then + PACK_ARGS+=("--buildpack" "${1#--buildpack=}") + elif [[ "$1" == "-b=*" ]]; then + PACK_ARGS+=("--buildpack" "${1#-b=}") + fi + shift 1 + ;; + -r=* | --buildpack-registry=*) + if [[ "$1" == "--buildpack-registry=*" ]]; then + PACK_ARGS+=("--buildpack-registry" "${1#--buildpack-registry=}") + elif [[ "$1" == "-r=*" ]]; then + PACK_ARGS+=("--buildpack-registry" "${1#-r=}") + fi + shift 2 + ;; + -D=* | --default-process=*) + if [[ "$1" == "--default-process=*" ]]; then + PACK_ARGS+=("--default-process" "${1#--default-process=}") + elif [[ "$1" == "-D=*" ]]; then + PACK_ARGS+=("--default-process" "${1#-D=}") + fi + shift 1 + ;; + -d=* | --descriptor=*) + if [[ "$1" == "--descriptor=*" ]]; then + PACK_ARGS+=("--descriptor" "${1#--descriptor=}") + elif [[ "$1" == "-d=*" ]]; then + PACK_ARGS+=("--descriptor" "${1#-d=}") + fi + shift 1 + ;; + -e=* | --env=*) + if [[ "$1" == "--env=*" ]]; then + PACK_ARGS+=("--env" "${1#--env=}") + elif [[ "$1" == "-e=*" ]]; then + PACK_ARGS+=("--env" "${1#-e=}") + fi + shift 1 + ;; + -p=* | --path=*) + if [[ "$1" == "--path=*" ]]; then + PACK_ARGS+=("--path" "${1#--path=}") + elif [[ "$1" == "-p=*" ]]; then + PACK_ARGS+=("--path" "${1#-p=}") + fi + shift 1 + ;; + -t=* | --tag=*) + if [[ "$1" == "--tag=*" ]]; then + PACK_ARGS+=("--tag" "${1#--tag=}") + elif [[ "$1" == "-t=*" ]]; then + PACK_ARGS+=("--tag" "${1#-t=}") + fi + shift 1 + ;; + --volume=* | -v=*) + if [[ "$1" == "--volume=*" ]]; then + PACK_ARGS+=("--volume" "${1#--volume=}") + elif [[ "$1" == "-v=*" ]]; then + PACK_ARGS+=("--volume" "${1#-v=}") + fi + shift 1 + ;; *) - break + continue ;; esac done From 8cb59892207aea90adeefd32eb14a725904b7012 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 22:27:26 -0500 Subject: [PATCH 071/144] fix: remove manually set arguments --- plugins/builder-pack/builder-build | 44 +----------------------------- 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/plugins/builder-pack/builder-build b/plugins/builder-pack/builder-build index 7eb612280..7d303a143 100755 --- a/plugins/builder-pack/builder-build +++ b/plugins/builder-pack/builder-build @@ -38,23 +38,15 @@ trigger-builder-pack-builder-build() { plugn trigger pre-build "$BUILDER_TYPE" "$APP" "$SOURCECODE_WORK_DIR" local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$BUILDER_TYPE") - [[ "$DOKKU_TRACE" ]] && DOCKER_ARGS+=" --env=TRACE=true " DOCKER_ARGS+=$(: | plugn trigger docker-args-process-build "$APP" "$BUILDER_TYPE") + [[ "$DOKKU_TRACE" ]] && DOCKER_ARGS+=" --env=TRACE=true " DOCKER_ARGS=" $DOCKER_ARGS " - declare -a ARG_ARRAY - eval "ARG_ARRAY=($DOCKER_ARGS)" - eval set -- "$DOCKER_ARGS" declare -a PACK_ARGS while true; do case "$1" in - -B | --builder) - PACK_ARGS+=("--builder") - PACK_ARGS+=("$2") - shift 2 - ;; -b | --buildpack) PACK_ARGS+=("--buildpack") PACK_ARGS+=("$2") @@ -65,11 +57,6 @@ trigger-builder-pack-builder-build() { PACK_ARGS+=("$2") shift 2 ;; - -D | --default-process) - PACK_ARGS+=("--default-process") - PACK_ARGS+=("$2") - shift 2 - ;; -d | --descriptor) PACK_ARGS+=("--descriptor") PACK_ARGS+=("$2") @@ -80,11 +67,6 @@ trigger-builder-pack-builder-build() { PACK_ARGS+=("$2") shift 2 ;; - -p | --path) - PACK_ARGS+=("--path") - PACK_ARGS+=("$2") - shift 2 - ;; -t | --tag) PACK_ARGS+=("--tag") PACK_ARGS+=("$2") @@ -104,14 +86,6 @@ trigger-builder-pack-builder-build() { PACK_ARGS+=("$2") shift 2 ;; - -B=* | --builder=*) - if [[ "$1" == "--builder=*" ]]; then - PACK_ARGS+=("--builder" "${1#--builder=}") - elif [[ "$1" == "-B=*" ]]; then - PACK_ARGS+=("--builder" "${1#-B=}") - fi - shift 1 - ;; -b=* | --buildpack=*) if [[ "$1" == "--buildpack=*" ]]; then PACK_ARGS+=("--buildpack" "${1#--buildpack=}") @@ -128,14 +102,6 @@ trigger-builder-pack-builder-build() { fi shift 2 ;; - -D=* | --default-process=*) - if [[ "$1" == "--default-process=*" ]]; then - PACK_ARGS+=("--default-process" "${1#--default-process=}") - elif [[ "$1" == "-D=*" ]]; then - PACK_ARGS+=("--default-process" "${1#-D=}") - fi - shift 1 - ;; -d=* | --descriptor=*) if [[ "$1" == "--descriptor=*" ]]; then PACK_ARGS+=("--descriptor" "${1#--descriptor=}") @@ -152,14 +118,6 @@ trigger-builder-pack-builder-build() { fi shift 1 ;; - -p=* | --path=*) - if [[ "$1" == "--path=*" ]]; then - PACK_ARGS+=("--path" "${1#--path=}") - elif [[ "$1" == "-p=*" ]]; then - PACK_ARGS+=("--path" "${1#-p=}") - fi - shift 1 - ;; -t=* | --tag=*) if [[ "$1" == "--tag=*" ]]; then PACK_ARGS+=("--tag" "${1#--tag=}") From 58046ea6dc7d43cb5d663a33379ea674d7c5d9e2 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 22:28:53 -0500 Subject: [PATCH 072/144] chore: move flags around for better organization --- plugins/builder-pack/builder-build | 68 +++++++++++++++--------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/plugins/builder-pack/builder-build b/plugins/builder-pack/builder-build index 7d303a143..237c54bde 100755 --- a/plugins/builder-pack/builder-build +++ b/plugins/builder-pack/builder-build @@ -52,40 +52,6 @@ trigger-builder-pack-builder-build() { PACK_ARGS+=("$2") shift 2 ;; - -r | --buildpack-registry) - PACK_ARGS+=("--buildpack-registry") - PACK_ARGS+=("$2") - shift 2 - ;; - -d | --descriptor) - PACK_ARGS+=("--descriptor") - PACK_ARGS+=("$2") - shift 2 - ;; - -e | --env) - PACK_ARGS+=("--env") - PACK_ARGS+=("$2") - shift 2 - ;; - -t | --tag) - PACK_ARGS+=("--tag") - PACK_ARGS+=("$2") - shift 2 - ;; - --cache | --cache-image | --creation-time | --docker-host | --env-file | --extension | --gid | --insecure-registry | --lifecycle-image | --network | --platform | --post-buildpack | --pre-buildpack | --previous-image | --pull-policy | --report-output-dir | --run-image | --sbom-output-dir | --uid | --workspace) - PACK_ARGS+=("$1") - PACK_ARGS+=("$2") - shift 2 - ;; - --clear-cache | --disable-system-buildpacks | --publish | --trust-builder | --trust-extra-buildpacks | --userns-host) - PACK_ARGS+=("$1") - shift 1 - ;; - --volume | -v) - PACK_ARGS+=("--volume") - PACK_ARGS+=("$2") - shift 2 - ;; -b=* | --buildpack=*) if [[ "$1" == "--buildpack=*" ]]; then PACK_ARGS+=("--buildpack" "${1#--buildpack=}") @@ -94,6 +60,11 @@ trigger-builder-pack-builder-build() { fi shift 1 ;; + -r | --buildpack-registry) + PACK_ARGS+=("--buildpack-registry") + PACK_ARGS+=("$2") + shift 2 + ;; -r=* | --buildpack-registry=*) if [[ "$1" == "--buildpack-registry=*" ]]; then PACK_ARGS+=("--buildpack-registry" "${1#--buildpack-registry=}") @@ -102,6 +73,11 @@ trigger-builder-pack-builder-build() { fi shift 2 ;; + -d | --descriptor) + PACK_ARGS+=("--descriptor") + PACK_ARGS+=("$2") + shift 2 + ;; -d=* | --descriptor=*) if [[ "$1" == "--descriptor=*" ]]; then PACK_ARGS+=("--descriptor" "${1#--descriptor=}") @@ -110,6 +86,11 @@ trigger-builder-pack-builder-build() { fi shift 1 ;; + -e | --env) + PACK_ARGS+=("--env") + PACK_ARGS+=("$2") + shift 2 + ;; -e=* | --env=*) if [[ "$1" == "--env=*" ]]; then PACK_ARGS+=("--env" "${1#--env=}") @@ -118,6 +99,11 @@ trigger-builder-pack-builder-build() { fi shift 1 ;; + -t | --tag) + PACK_ARGS+=("--tag") + PACK_ARGS+=("$2") + shift 2 + ;; -t=* | --tag=*) if [[ "$1" == "--tag=*" ]]; then PACK_ARGS+=("--tag" "${1#--tag=}") @@ -126,6 +112,11 @@ trigger-builder-pack-builder-build() { fi shift 1 ;; + --volume | -v) + PACK_ARGS+=("--volume") + PACK_ARGS+=("$2") + shift 2 + ;; --volume=* | -v=*) if [[ "$1" == "--volume=*" ]]; then PACK_ARGS+=("--volume" "${1#--volume=}") @@ -134,6 +125,15 @@ trigger-builder-pack-builder-build() { fi shift 1 ;; + --cache | --cache-image | --creation-time | --docker-host | --env-file | --extension | --gid | --insecure-registry | --lifecycle-image | --network | --platform | --post-buildpack | --pre-buildpack | --previous-image | --pull-policy | --report-output-dir | --run-image | --sbom-output-dir | --uid | --workspace) + PACK_ARGS+=("$1") + PACK_ARGS+=("$2") + shift 2 + ;; + --clear-cache | --disable-system-buildpacks | --publish | --trust-builder | --trust-extra-buildpacks | --userns-host) + PACK_ARGS+=("$1") + shift 1 + ;; *) continue ;; From a292e41015147673bbe76c274dec64b69c801a76 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 22:33:03 -0500 Subject: [PATCH 073/144] fix: remove flags that do not make sense in build context --- plugins/builder-pack/builder-build | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/builder-pack/builder-build b/plugins/builder-pack/builder-build index 237c54bde..59554dada 100755 --- a/plugins/builder-pack/builder-build +++ b/plugins/builder-pack/builder-build @@ -125,7 +125,8 @@ trigger-builder-pack-builder-build() { fi shift 1 ;; - --cache | --cache-image | --creation-time | --docker-host | --env-file | --extension | --gid | --insecure-registry | --lifecycle-image | --network | --platform | --post-buildpack | --pre-buildpack | --previous-image | --pull-policy | --report-output-dir | --run-image | --sbom-output-dir | --uid | --workspace) + --cache | --cache-image | --creation-time | --env-file | --extension | --gid | --insecure-registry | --lifecycle-image | --network | --platform | --post-buildpack | --pre-buildpack | --previous-image | --pull-policy | --run-image | --uid | --workspace) + # todo: additionally support --flag=value syntax PACK_ARGS+=("$1") PACK_ARGS+=("$2") shift 2 From c3d103e46c9b9c5eb65a6ae748058694e5eaf893 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 22:39:27 -0500 Subject: [PATCH 074/144] fix: remove flag that does not make sense in build context --- plugins/builder-pack/builder-build | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/plugins/builder-pack/builder-build b/plugins/builder-pack/builder-build index 59554dada..e3d3eef84 100755 --- a/plugins/builder-pack/builder-build +++ b/plugins/builder-pack/builder-build @@ -73,19 +73,6 @@ trigger-builder-pack-builder-build() { fi shift 2 ;; - -d | --descriptor) - PACK_ARGS+=("--descriptor") - PACK_ARGS+=("$2") - shift 2 - ;; - -d=* | --descriptor=*) - if [[ "$1" == "--descriptor=*" ]]; then - PACK_ARGS+=("--descriptor" "${1#--descriptor=}") - elif [[ "$1" == "-d=*" ]]; then - PACK_ARGS+=("--descriptor" "${1#-d=}") - fi - shift 1 - ;; -e | --env) PACK_ARGS+=("--env") PACK_ARGS+=("$2") From 03e90ed2a0678e6576c7da7bce433eb14d6acd75 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 22:49:48 -0500 Subject: [PATCH 075/144] feat: filter out unsupported build arguments with nixpacks While this will require future updates if nixpacks supports new build arguments, it also ensures builds safely consume arguments from other plugins. --- plugins/builder-nixpacks/builder-build | 132 ++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 5 deletions(-) diff --git a/plugins/builder-nixpacks/builder-build b/plugins/builder-nixpacks/builder-build index c27a44921..d2eede6a3 100755 --- a/plugins/builder-nixpacks/builder-build +++ b/plugins/builder-nixpacks/builder-build @@ -35,11 +35,133 @@ trigger-builder-nixpacks-builder-build() { local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$BUILDER_TYPE") DOCKER_ARGS+=$(: | plugn trigger docker-args-process-build "$APP" "$BUILDER_TYPE") + DOCKER_ARGS+=" $DOKKU_GLOBAL_BUILD_ARGS" - # strip --link, --volume and -v args from DOCKER_ARGS - local DOCKER_ARGS=$(sed -e "s/^--link=[[:graph:]]\+[[:blank:]]\?//g" -e "s/^--link[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" -e "s/^--volume=[[:graph:]]\+[[:blank:]]\?//g" -e "s/^--volume[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" -e "s/^-v[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" <<<"$DOCKER_ARGS") - declare -a ARG_ARRAY - eval "ARG_ARRAY=($DOCKER_ARGS)" + DOCKER_ARGS=" $DOCKER_ARGS " + eval set -- "$DOCKER_ARGS" + + declare -a NIXPACKS_ARGS + while true; do + case "$1" in + -t | --tag) + NIXPACKS_ARGS+=("--tag") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + -t=* | --tag=*) + if [[ "$1" == "--tag=*" ]]; then + NIXPACKS_ARGS+=("--tag" "${1#--tag=}") + elif [[ "$1" == "-t=*" ]]; then + NIXPACKS_ARGS+=("--tag" "${1#-t=}") + fi + shift 1 + ;; + -i | --install-cmd) + NIXPACKS_ARGS+=("--install-cmd") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + -i=* | --install-cmd=*) + if [[ "$1" == "--install-cmd=*" ]]; then + NIXPACKS_ARGS+=("--install-cmd" "${1#--install-cmd=}") + elif [[ "$1" == "-i=*" ]]; then + NIXPACKS_ARGS+=("--install-cmd" "${1#-i=}") + fi + shift 1 + ;; + -l | --label) + NIXPACKS_ARGS+=("--label") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + -l=* | --label=*) + if [[ "$1" == "--label=*" ]]; then + NIXPACKS_ARGS+=("--label" "${1#--label=}") + elif [[ "$1" == "-l=*" ]]; then + NIXPACKS_ARGS+=("--label" "${1#-l=}") + fi + shift 1 + ;; + -b | --build-cmd) + NIXPACKS_ARGS+=("--build-cmd") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + -b=* | --build-cmd=*) + if [[ "$1" == "--build-cmd=*" ]]; then + NIXPACKS_ARGS+=("--build-cmd" "${1#--build-cmd=}") + elif [[ "$1" == "-b=*" ]]; then + NIXPACKS_ARGS+=("--build-cmd" "${1#-b=}") + fi + shift 1 + ;; + -s | --start-cmd) + NIXPACKS_ARGS+=("--start-cmd") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + -s=* | --start-cmd=*) + if [[ "$1" == "--start-cmd=*" ]]; then + NIXPACKS_ARGS+=("--start-cmd" "${1#--start-cmd=}") + elif [[ "$1" == "-s=*" ]]; then + NIXPACKS_ARGS+=("--start-cmd" "${1#-s=}") + fi + shift 1 + ;; + -p | --pkgs) + NIXPACKS_ARGS+=("--pkgs") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + -p=* | --pkgs=*) + if [[ "$1" == "--pkgs=*" ]]; then + NIXPACKS_ARGS+=("--pkgs" "${1#--pkgs=}") + elif [[ "$1" == "-p=*" ]]; then + NIXPACKS_ARGS+=("--pkgs" "${1#-p=}") + fi + shift 1 + ;; + -a | --apt) + NIXPACKS_ARGS+=("--apt") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + -a=* | --apt=*) + if [[ "$1" == "--apt=*" ]]; then + NIXPACKS_ARGS+=("--apt" "${1#--apt=}") + elif [[ "$1" == "-a=*" ]]; then + NIXPACKS_ARGS+=("--apt" "${1#-a=}") + fi + shift 1 + ;; + -e | --env) + NIXPACKS_ARGS+=("--env") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + -e=* | --env=*) + if [[ "$1" == "--env=*" ]]; then + NIXPACKS_ARGS+=("--env" "${1#--env=}") + elif [[ "$1" == "-e=*" ]]; then + NIXPACKS_ARGS+=("--env" "${1#-e=}") + fi + shift 1 + ;; + --platform | --cache-key | --incremental-cache-image | --libs | --cache-from | --docker-host | --add-host | --docker-tls-verify) + # todo: additionally support --flag=value syntax + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --no-cache | --inline-cache | --no-error-without-start) + NIXPACKS_ARGS+=("$1") + shift 1 + ;; + *) + continue + ;; + esac + done eval "$(config_export app "$APP" --merged)" @@ -50,7 +172,7 @@ trigger-builder-nixpacks-builder-build() { fi # shellcheck disable=SC2086 - if ! nixpacks build "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS "${ARG_ARRAY[@]}" $NIXPACKS_ARGS --name "$IMAGE" "$SOURCECODE_WORK_DIR"; then + if ! nixpacks build "${DOCKER_BUILD_LABEL_ARGS[@]}" "${NIXPACKS_ARGS[@]}" --name "$IMAGE" "$SOURCECODE_WORK_DIR"; then dokku_log_warn "Failure building image" return 1 fi From 11c2497b311ed5a1b81ae3d903587f5d0502b774 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 22:59:12 -0500 Subject: [PATCH 076/144] feat: filter out unsupported build arguments with railpacks While this will require future updates if railpacks supports new build arguments, it also ensures builds safely consume arguments from other plugins. --- plugins/builder-railpack/builder-build | 121 +++++++++++++++++++++---- 1 file changed, 104 insertions(+), 17 deletions(-) diff --git a/plugins/builder-railpack/builder-build b/plugins/builder-railpack/builder-build index e0716522d..5d4c8faf0 100755 --- a/plugins/builder-railpack/builder-build +++ b/plugins/builder-railpack/builder-build @@ -27,30 +27,117 @@ trigger-builder-railpack-builder-build() { plugn trigger pre-build "$BUILDER_TYPE" "$APP" "$SOURCECODE_WORK_DIR" - no_cache="$(fn-builder-railpack-computed-no-cache "$APP")" - RAILPACK_ARGS="" - if [[ "$no_cache" == "true" ]]; then - RAILPACK_ARGS="$RAILPACK_ARGS --no-cache" - fi - - local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$BUILDER_TYPE") - DOCKER_ARGS+=$(: | plugn trigger docker-args-process-build "$APP" "$BUILDER_TYPE") - - # strip --link, --volume and -v args from DOCKER_ARGS - local DOCKER_ARGS=$(sed -e "s/^--link=[[:graph:]]\+[[:blank:]]\?//g" -e "s/^--link[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" -e "s/^--volume=[[:graph:]]\+[[:blank:]]\?//g" -e "s/^--volume[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" -e "s/^-v[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" <<<"$DOCKER_ARGS") - declare -a ARG_ARRAY - eval "ARG_ARRAY=($DOCKER_ARGS)" - - eval "$(config_export app "$APP" --merged)" - if [[ -f "$SOURCECODE_WORK_DIR/Procfile" ]]; then if procfile-util exists --process-type release; then procfile-util delete --process-type release fi fi + local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$BUILDER_TYPE") + DOCKER_ARGS+=$(: | plugn trigger docker-args-process-build "$APP" "$BUILDER_TYPE") + [[ "$(fn-builder-railpack-computed-no-cache "$APP")" == "true" ]] && DOCKER_ARGS+=" --no-cache" + + DOCKER_ARGS=" $DOCKER_ARGS " + eval set -- "$DOCKER_ARGS" + + declare -a RAILPACK_ARGS + while true; do + case "$1" in + --platform) + RAILPACK_ARGS+=("--platform") + RAILPACK_ARGS+=("$2") + shift 2 + ;; + --platform=*) + if [[ "$1" == "--buildpack=*" ]]; then + RAILPACK_ARGS+=("--platform" "${1#--platform=}") + elif [[ "$1" == "--platform=*" ]]; then + RAILPACK_ARGS+=("--platform" "${1#--platform=}") + fi + shift 1 + ;; + --progress) + RAILPACK_ARGS+=("--progress") + RAILPACK_ARGS+=("$2") + shift 2 + ;; + --progress=*) + if [[ "$1" == "--progress=*" ]]; then + RAILPACK_ARGS+=("--progress" "${1#--progress=}") + fi + shift 1 + ;; + --cache-key) + RAILPACK_ARGS+=("--cache-key") + RAILPACK_ARGS+=("$2") + shift 2 + ;; + --cache-key=*) + if [[ "$1" == "--cache-key=*" ]]; then + RAILPACK_ARGS+=("--cache-key" "${1#--cache-key=}") + fi + shift 1 + ;; + -e | --env) + RAILPACK_ARGS+=("--env") + RAILPACK_ARGS+=("$2") + shift 2 + ;; + -e=* | --env=*) + if [[ "$1" == "--env=*" ]]; then + RAILPACK_ARGS+=("--env" "${1#--env=}") + elif [[ "$1" == "--env=*" ]]; then + RAILPACK_ARGS+=("--env" "${1#--env=}") + fi + shift 1 + ;; + --previous) + RAILPACK_ARGS+=("--previous") + RAILPACK_ARGS+=("$2") + shift 2 + ;; + --previous=*) + if [[ "$1" == "--previous=*" ]]; then + RAILPACK_ARGS+=("--previous" "${1#--previous=}") + fi + shift 1 + ;; + --build-cmd) + RAILPACK_ARGS+=("--build-cmd") + RAILPACK_ARGS+=("$2") + shift 2 + ;; + --build-cmd=*) + if [[ "$1" == "--build-cmd=*" ]]; then + RAILPACK_ARGS+=("--build-cmd" "${1#--build-cmd=}") + fi + shift 1 + ;; + --start-cmd) + RAILPACK_ARGS+=("--start-cmd") + RAILPACK_ARGS+=("$2") + shift 2 + ;; + --start-cmd=*) + if [[ "$1" == "--start-cmd=*" ]]; then + RAILPACK_ARGS+=("--start-cmd" "${1#--start-cmd=}") + fi + shift 1 + ;; + ---show-plan | --error-missing-start) + RAILPACK_ARGS+=("$1") + shift 1 + ;; + *) + continue + ;; + esac + done + + eval "$(config_export app "$APP" --merged)" + # shellcheck disable=SC2086 - if ! railpack build "${ARG_ARRAY[@]}" $RAILPACK_ARGS --name "$IMAGE-build" "$SOURCECODE_WORK_DIR"; then + if ! railpack build "${RAILPACK_ARGS[@]}" --name "$IMAGE-build" "$SOURCECODE_WORK_DIR"; then dokku_log_warn "Failure building image" return 1 fi From ae8eaa7e63e88c9f05569265c6ce39dc58c16176 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 22:59:49 -0500 Subject: [PATCH 077/144] refactor: filter out procfile first and include --no-cache in DOCKER_ARGS --- plugins/builder-nixpacks/builder-build | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/plugins/builder-nixpacks/builder-build b/plugins/builder-nixpacks/builder-build index d2eede6a3..d1ab7340e 100755 --- a/plugins/builder-nixpacks/builder-build +++ b/plugins/builder-nixpacks/builder-build @@ -27,15 +27,16 @@ trigger-builder-nixpacks-builder-build() { plugn trigger pre-build "$BUILDER_TYPE" "$APP" "$SOURCECODE_WORK_DIR" - no_cache="$(fn-builder-nixpacks-computed-no-cache "$APP")" - NIXPACKS_ARGS="" - if [[ "$no_cache" == "true" ]]; then - NIXPACKS_ARGS="$NIXPACKS_ARGS --no-cache" + if [[ -f "$SOURCECODE_WORK_DIR/Procfile" ]]; then + if procfile-util exists --process-type release; then + procfile-util delete --process-type release + fi fi local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$BUILDER_TYPE") DOCKER_ARGS+=$(: | plugn trigger docker-args-process-build "$APP" "$BUILDER_TYPE") DOCKER_ARGS+=" $DOKKU_GLOBAL_BUILD_ARGS" + [[ "$(fn-builder-nixpacks-computed-no-cache "$APP")" == "true" ]] && DOCKER_ARGS+=" --no-cache" DOCKER_ARGS=" $DOCKER_ARGS " eval set -- "$DOCKER_ARGS" @@ -165,12 +166,6 @@ trigger-builder-nixpacks-builder-build() { eval "$(config_export app "$APP" --merged)" - if [[ -f "$SOURCECODE_WORK_DIR/Procfile" ]]; then - if procfile-util exists --process-type release; then - procfile-util delete --process-type release - fi - fi - # shellcheck disable=SC2086 if ! nixpacks build "${DOCKER_BUILD_LABEL_ARGS[@]}" "${NIXPACKS_ARGS[@]}" --name "$IMAGE" "$SOURCECODE_WORK_DIR"; then dokku_log_warn "Failure building image" From 210128f5f25baf494c51b5c076e2552e8dfc66b3 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 23:13:57 -0500 Subject: [PATCH 078/144] chore: simplify logic for flags without multiple methods of being set --- plugins/builder-railpack/builder-build | 30 +++++++------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/plugins/builder-railpack/builder-build b/plugins/builder-railpack/builder-build index 5d4c8faf0..68d2698f3 100755 --- a/plugins/builder-railpack/builder-build +++ b/plugins/builder-railpack/builder-build @@ -49,11 +49,7 @@ trigger-builder-railpack-builder-build() { shift 2 ;; --platform=*) - if [[ "$1" == "--buildpack=*" ]]; then - RAILPACK_ARGS+=("--platform" "${1#--platform=}") - elif [[ "$1" == "--platform=*" ]]; then - RAILPACK_ARGS+=("--platform" "${1#--platform=}") - fi + RAILPACK_ARGS+=("--platform" "${1#--platform=}") shift 1 ;; --progress) @@ -62,9 +58,7 @@ trigger-builder-railpack-builder-build() { shift 2 ;; --progress=*) - if [[ "$1" == "--progress=*" ]]; then - RAILPACK_ARGS+=("--progress" "${1#--progress=}") - fi + RAILPACK_ARGS+=("--progress" "${1#--progress=}") shift 1 ;; --cache-key) @@ -73,9 +67,7 @@ trigger-builder-railpack-builder-build() { shift 2 ;; --cache-key=*) - if [[ "$1" == "--cache-key=*" ]]; then - RAILPACK_ARGS+=("--cache-key" "${1#--cache-key=}") - fi + RAILPACK_ARGS+=("--cache-key" "${1#--cache-key=}") shift 1 ;; -e | --env) @@ -86,8 +78,8 @@ trigger-builder-railpack-builder-build() { -e=* | --env=*) if [[ "$1" == "--env=*" ]]; then RAILPACK_ARGS+=("--env" "${1#--env=}") - elif [[ "$1" == "--env=*" ]]; then - RAILPACK_ARGS+=("--env" "${1#--env=}") + elif [[ "$1" == "--e=*" ]]; then + RAILPACK_ARGS+=("--env" "${1#-e=}") fi shift 1 ;; @@ -97,9 +89,7 @@ trigger-builder-railpack-builder-build() { shift 2 ;; --previous=*) - if [[ "$1" == "--previous=*" ]]; then - RAILPACK_ARGS+=("--previous" "${1#--previous=}") - fi + RAILPACK_ARGS+=("--previous" "${1#--previous=}") shift 1 ;; --build-cmd) @@ -108,9 +98,7 @@ trigger-builder-railpack-builder-build() { shift 2 ;; --build-cmd=*) - if [[ "$1" == "--build-cmd=*" ]]; then - RAILPACK_ARGS+=("--build-cmd" "${1#--build-cmd=}") - fi + RAILPACK_ARGS+=("--build-cmd" "${1#--build-cmd=}") shift 1 ;; --start-cmd) @@ -119,9 +107,7 @@ trigger-builder-railpack-builder-build() { shift 2 ;; --start-cmd=*) - if [[ "$1" == "--start-cmd=*" ]]; then - RAILPACK_ARGS+=("--start-cmd" "${1#--start-cmd=}") - fi + RAILPACK_ARGS+=("--start-cmd" "${1#--start-cmd=}") shift 1 ;; ---show-plan | --error-missing-start) From 2a702175b55d490fe0e6a1ea8e7b097eb6bc7b20 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 23:17:48 -0500 Subject: [PATCH 079/144] feat: filter out unsupported build arguments with docker build While this will require future updates if docker supports new build arguments, it also ensures builds safely consume arguments from other plugins. --- plugins/builder-dockerfile/builder-build | 215 ++++++++++++++++++++++- 1 file changed, 208 insertions(+), 7 deletions(-) diff --git a/plugins/builder-dockerfile/builder-build b/plugins/builder-dockerfile/builder-build index 08a0d5d10..22ed24709 100755 --- a/plugins/builder-dockerfile/builder-build +++ b/plugins/builder-dockerfile/builder-build @@ -17,7 +17,6 @@ trigger-builder-dockerfile-builder-build() { dokku_log_info1 "Building $APP from Dockerfile" local IMAGE=$(get_app_image_name "$APP") - local DOKKU_DOCKERFILE_CACHE_BUILD=$(config_get "$APP" "DOKKU_DOCKERFILE_CACHE_BUILD") local DOKKU_DOCKER_BUILD_OPTS=$(config_get "$APP" "DOKKU_DOCKER_BUILD_OPTS") local DOCKER_BUILD_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=dockerfile" "--label=com.dokku.app-name=$APP") @@ -34,17 +33,219 @@ trigger-builder-dockerfile-builder-build() { fi plugn trigger pre-build "$BUILDER_TYPE" "$APP" "$SOURCECODE_WORK_DIR" - [[ "$DOKKU_DOCKERFILE_CACHE_BUILD" == "false" ]] && DOKKU_DOCKER_BUILD_OPTS="$DOKKU_DOCKER_BUILD_OPTS --no-cache" local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$BUILDER_TYPE") DOCKER_ARGS+=$(: | plugn trigger docker-args-process-build "$APP" "$BUILDER_TYPE") + DOCKER_ARGS+=" $DOKKU_GLOBAL_BUILD_ARGS" + [[ "$(config_get "$APP" "DOKKU_DOCKERFILE_CACHE_BUILD")" == "false" ]] && DOCKER_ARGS+=" --no-cache" - # strip --link, --volume and -v args from DOCKER_ARGS - local DOCKER_ARGS=$(sed -e "s/^--link=[[:graph:]]\+[[:blank:]]\?//g" -e "s/^--link[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" -e "s/^--volume=[[:graph:]]\+[[:blank:]]\?//g" -e "s/^--volume[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" -e "s/^-v[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" <<<"$DOCKER_ARGS") - declare -a ARG_ARRAY - eval "ARG_ARRAY=($DOCKER_ARGS)" + + DOCKER_ARGS=" $DOCKER_ARGS " + eval set -- "$DOCKER_ARGS" + + declare -a DOCKERFILE_ARGS + while true; do + case "$1" in + --add-host) + DOCKERFILE_ARGS+=("--add-host") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --add-host=*) + DOCKERFILE_ARGS+=("--add-host" "${1#--add-host=}") + shift 1 + ;; + --allow) + DOCKERFILE_ARGS+=("--allow") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --allow=*) + DOCKERFILE_ARGS+=("--allow" "${1#--allow=}") + shift 1 + ;; + --annotation) + DOCKERFILE_ARGS+=("--annotation") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --annotation=*) + DOCKERFILE_ARGS+=("--annotation" "${1#--annotation=}") + shift 1 + ;; + --attest) + DOCKERFILE_ARGS+=("--attest") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --attest=*) + DOCKERFILE_ARGS+=("--attest" "${1#--attest=}") + shift 1 + ;; + --build-arg) + DOCKERFILE_ARGS+=("--build-arg") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --build-arg=*) + DOCKERFILE_ARGS+=("--build-arg" "${1#--build-arg=}") + shift 1 + ;; + --builder) + DOCKERFILE_ARGS+=("--builder") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --builder=*) + DOCKERFILE_ARGS+=("--builder" "${1#--builder=}") + shift 1 + ;; + --cache-from) + DOCKERFILE_ARGS+=("--cache-from") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --cache-from=*) + DOCKERFILE_ARGS+=("--cache-from" "${1#--cache-from=}") + shift 1 + ;; + --cache-to) + DOCKERFILE_ARGS+=("--cache-to") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --cache-to=*) + DOCKERFILE_ARGS+=("--cache-to" "${1#--cache-to=}") + shift 1 + ;; + --call) + DOCKERFILE_ARGS+=("--env") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --env=*) + DOCKERFILE_ARGS+=("--env" "${1#--env=}") + shift 1 + ;; + --cgroup-parent) + DOCKERFILE_ARGS+=("--cgroup-parent") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --cgroup-parent=*) + DOCKERFILE_ARGS+=("--cgroup-parent" "${1#--cgroup-parent=}") + shift 1 + ;; + --label) + DOCKERFILE_ARGS+=("--label") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --label=*) + DOCKERFILE_ARGS+=("--label" "${1#--label=}") + shift 1 + ;; + --network) + DOCKERFILE_ARGS+=("--network") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --network=*) + DOCKERFILE_ARGS+=("--network" "${1#--network=}") + shift 1 + ;; + --platform) + DOCKERFILE_ARGS+=("--platform") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --platform=*) + DOCKERFILE_ARGS+=("--platform" "${1#--platform=}") + shift 1 + ;; + --progress) + DOCKERFILE_ARGS+=("--progress") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --progress=*) + DOCKERFILE_ARGS+=("--progress" "${1#--progress=}") + shift 1 + ;; + --provenance) + DOCKERFILE_ARGS+=("--provenance") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --provenance=*) + DOCKERFILE_ARGS+=("--provenance" "${1#--provenance=}") + shift 1 + ;; + --sbom) + DOCKERFILE_ARGS+=("--sbom") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --sbom=*) + DOCKERFILE_ARGS+=("--sbom" "${1#--sbom=}") + shift 1 + ;; + --secret) + DOCKERFILE_ARGS+=("--secret") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --secret=*) + DOCKERFILE_ARGS+=("--secret" "${1#--secret=}") + shift 1 + ;; + --shm-size) + DOCKERFILE_ARGS+=("--shm-size") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --shm-size=*) + DOCKERFILE_ARGS+=("--shm-size" "${1#--shm-size=}") + shift 1 + ;; + --ssh) + DOCKERFILE_ARGS+=("--platform") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --ssh=*) + DOCKERFILE_ARGS+=("--ssh" "${1#--ssh=}") + shift 1 + ;; + --target) + DOCKERFILE_ARGS+=("--target") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --target=*) + DOCKERFILE_ARGS+=("--target" "${1#--target=}") + shift 1 + ;; + --ulimit) + DOCKERFILE_ARGS+=("--tag") + DOCKERFILE_ARGS+=("$2") + shift 2 + ;; + --tag=*) + DOCKERFILE_ARGS+=("--tag" "${1#--tag=}") + shift 1 + ;; + --check | -D | --debug | --no-cache) + DOCKERFILE_ARGS+=("$1") + shift 1 + ;; + *) + continue + ;; + esac + done eval "$(config_export app "$APP")" - "$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS "${ARG_ARRAY[@]}" ${DOKKU_DOCKER_BUILD_OPTS} -t $IMAGE . + "$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" "${DOCKERFILE_ARGS[@]}" ${DOKKU_DOCKER_BUILD_OPTS} --tag $IMAGE . plugn trigger ports-set-detected "$APP" "$(fn-builder-dockerfile-get-detect-port-map "$APP" "$IMAGE" "$SOURCECODE_WORK_DIR/Dockerfile")" if fn-plugn-trigger-exists "post-build-dockerfile"; then From 6325dd6d7bf7a0d8498cad8a82f5f78ee656d3c6 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 23:21:41 -0500 Subject: [PATCH 080/144] docs: add missing link to 0.36.0 migration guide --- docs/getting-started/upgrading/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/getting-started/upgrading/index.md b/docs/getting-started/upgrading/index.md index 7effb6010..166bea94b 100644 --- a/docs/getting-started/upgrading/index.md +++ b/docs/getting-started/upgrading/index.md @@ -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.36](/docs/appendices/0.36.0-migration-guide.md) - [Upgrading to 0.35](/docs/appendices/0.35.0-migration-guide.md) - [Upgrading to 0.34](/docs/appendices/0.34.0-migration-guide.md) - [Upgrading to 0.33](/docs/appendices/0.33.0-migration-guide.md) From 9b75cc39b1ebb527f7fa81d5c69cbaf51718be33 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 23:27:22 -0500 Subject: [PATCH 081/144] refactor: remove support for DOKKU_DOCKERFILE_CACHE_BUILD This replicates the docker-options functionality. --- docs/appendices/0.37.0-migration-guide.md | 5 +++++ docs/configuration/environment-variables.md | 1 - docs/deployment/builders/dockerfiles.md | 2 +- docs/getting-started/upgrading/index.md | 1 + plugins/builder-dockerfile/builder-build | 1 - 5 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 docs/appendices/0.37.0-migration-guide.md diff --git a/docs/appendices/0.37.0-migration-guide.md b/docs/appendices/0.37.0-migration-guide.md new file mode 100644 index 000000000..c7a07eee8 --- /dev/null +++ b/docs/appendices/0.37.0-migration-guide.md @@ -0,0 +1,5 @@ +# 0.36.0 Migration Guide + +## Removals + +- Support for the `DOKKU_DOCKERFILE_CACHE_BUILD` environment variable has been removed. Use the `docker-options` plugin to set build arguments for your app instead. \ No newline at end of file diff --git a/docs/configuration/environment-variables.md b/docs/configuration/environment-variables.md index d2c21fd50..dd805bd0f 100644 --- a/docs/configuration/environment-variables.md +++ b/docs/configuration/environment-variables.md @@ -110,7 +110,6 @@ The following config variables have special meanings and can be set in a variety | `DOKKU_DEFAULT_CHECKS_WAIT` | `10` | `dokku config:set` | If no user-defined checks are specified - or if the process being checked is not a `web` process - this is the period of time Dokku will wait before checking that a container is still running. | | `DOKKU_DISABLE_PROXY` | none | `dokku proxy:disable`
`dokku proxy:enable` | Disables the proxy in front of your application, resulting in publicly routing the docker container. | | `DOKKU_DISABLE_APP_AUTOCREATION` | none | `dokku config:set` | Disables automatic creation of a non-existent app on deploy. | -| `DOKKU_DOCKERFILE_CACHE_BUILD` | none | `dokku config:set` | | | `DOKKU_DOCKERFILE_START_CMD` | none | `dokku config:set` | | | `DOKKU_PARALLEL_ARGUMENTS`. | none | `dokku config:set` | Allows passing custom arguments to parallel for `ps:*all` commands | | `DOKKU_PROXY_PORT` | automatically assigned | `/etc/environment`
`~dokku/.dokkurc`
`~dokku/.dokkurc/*`
`dokku config:set` | | diff --git a/docs/deployment/builders/dockerfiles.md b/docs/deployment/builders/dockerfiles.md index 64b3c4726..a7a49b080 100644 --- a/docs/deployment/builders/dockerfiles.md +++ b/docs/deployment/builders/dockerfiles.md @@ -199,7 +199,7 @@ dokku config:set node-js-app DOKKU_DOCKERFILE_START_CMD="--harmony server.js" To tell Docker what to run. -Setting `$DOKKU_DOCKERFILE_CACHE_BUILD` to `true` or `false` will enable or disable Docker's image layer cache. Lastly, for more granular build control, you may also pass any `docker build` option to `docker`, by setting `$DOKKU_DOCKER_BUILD_OPTS`. +For more granular build control, you may also pass any `docker build` option to `docker`, by setting `$DOKKU_DOCKER_BUILD_OPTS`. ### Procfiles and multiple processes diff --git a/docs/getting-started/upgrading/index.md b/docs/getting-started/upgrading/index.md index 166bea94b..7c3ae989d 100644 --- a/docs/getting-started/upgrading/index.md +++ b/docs/getting-started/upgrading/index.md @@ -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.37](/docs/appendices/0.37.0-migration-guide.md) - [Upgrading to 0.36](/docs/appendices/0.36.0-migration-guide.md) - [Upgrading to 0.35](/docs/appendices/0.35.0-migration-guide.md) - [Upgrading to 0.34](/docs/appendices/0.34.0-migration-guide.md) diff --git a/plugins/builder-dockerfile/builder-build b/plugins/builder-dockerfile/builder-build index 22ed24709..5916461f3 100755 --- a/plugins/builder-dockerfile/builder-build +++ b/plugins/builder-dockerfile/builder-build @@ -36,7 +36,6 @@ trigger-builder-dockerfile-builder-build() { local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$BUILDER_TYPE") DOCKER_ARGS+=$(: | plugn trigger docker-args-process-build "$APP" "$BUILDER_TYPE") DOCKER_ARGS+=" $DOKKU_GLOBAL_BUILD_ARGS" - [[ "$(config_get "$APP" "DOKKU_DOCKERFILE_CACHE_BUILD")" == "false" ]] && DOCKER_ARGS+=" --no-cache" DOCKER_ARGS=" $DOCKER_ARGS " From 50b2836e52ac6c902c1d0116bf94b2b68cf0ef40 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 23:28:44 -0500 Subject: [PATCH 082/144] refactor: remove support for DOKKU_DOCKER_BUILD_OPTS This replicates the docker-options functionality. --- docs/appendices/0.37.0-migration-guide.md | 3 ++- docs/deployment/builders/dockerfiles.md | 2 -- plugins/builder-dockerfile/builder-build | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/appendices/0.37.0-migration-guide.md b/docs/appendices/0.37.0-migration-guide.md index c7a07eee8..bc7943389 100644 --- a/docs/appendices/0.37.0-migration-guide.md +++ b/docs/appendices/0.37.0-migration-guide.md @@ -2,4 +2,5 @@ ## Removals -- Support for the `DOKKU_DOCKERFILE_CACHE_BUILD` environment variable has been removed. Use the `docker-options` plugin to set build arguments for your app instead. \ No newline at end of file +- Support for the `DOKKU_DOCKERFILE_CACHE_BUILD` environment variable has been removed. Use the `docker-options` plugin to set build arguments for your app instead. +- Support for the `DOKKU_DOCKER_BUILD_OPTS` environment variable has been removed. Use the `docker-options` plugin to set build arguments for your app instead. diff --git a/docs/deployment/builders/dockerfiles.md b/docs/deployment/builders/dockerfiles.md index a7a49b080..ec1b7b5ee 100644 --- a/docs/deployment/builders/dockerfiles.md +++ b/docs/deployment/builders/dockerfiles.md @@ -199,8 +199,6 @@ dokku config:set node-js-app DOKKU_DOCKERFILE_START_CMD="--harmony server.js" To tell Docker what to run. -For more granular build control, you may also pass any `docker build` option to `docker`, by setting `$DOKKU_DOCKER_BUILD_OPTS`. - ### Procfiles and multiple processes > [!IMPORTANT] diff --git a/plugins/builder-dockerfile/builder-build b/plugins/builder-dockerfile/builder-build index 5916461f3..56852547c 100755 --- a/plugins/builder-dockerfile/builder-build +++ b/plugins/builder-dockerfile/builder-build @@ -17,7 +17,6 @@ trigger-builder-dockerfile-builder-build() { dokku_log_info1 "Building $APP from Dockerfile" local IMAGE=$(get_app_image_name "$APP") - local DOKKU_DOCKER_BUILD_OPTS=$(config_get "$APP" "DOKKU_DOCKER_BUILD_OPTS") local DOCKER_BUILD_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=dockerfile" "--label=com.dokku.app-name=$APP") pushd "$SOURCECODE_WORK_DIR" &>/dev/null @@ -244,7 +243,7 @@ trigger-builder-dockerfile-builder-build() { done eval "$(config_export app "$APP")" - "$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" "${DOCKERFILE_ARGS[@]}" ${DOKKU_DOCKER_BUILD_OPTS} --tag $IMAGE . + "$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" "${DOCKERFILE_ARGS[@]}" --tag $IMAGE . plugn trigger ports-set-detected "$APP" "$(fn-builder-dockerfile-get-detect-port-map "$APP" "$IMAGE" "$SOURCECODE_WORK_DIR/Dockerfile")" if fn-plugn-trigger-exists "post-build-dockerfile"; then From 93cf9cc1189d8d020d259d5dcd0d6957b72cec81 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 23:30:15 -0500 Subject: [PATCH 083/144] refactor: remove support for the nixpacks no-cache property This replicates the docker-options functionality. --- docs/deployment/builders/nixpacks.md | 38 +++------------------ plugins/builder-nixpacks/builder-build | 1 - plugins/builder-nixpacks/internal-functions | 26 -------------- plugins/builder-nixpacks/subcommands/set | 4 +-- 4 files changed, 6 insertions(+), 63 deletions(-) diff --git a/docs/deployment/builders/nixpacks.md b/docs/deployment/builders/nixpacks.md index 46559ca68..945b424c3 100644 --- a/docs/deployment/builders/nixpacks.md +++ b/docs/deployment/builders/nixpacks.md @@ -80,28 +80,10 @@ dokku builder-nixpacks:set --global nixpackstoml-path ### Disabling cache -Cache is enabled by default, but can be disabled by setting the `no-cache` property to `true`: +Disable cache using the [docker-options] plugin: ```shell -dokku builder-nixpacks:set node-js-app no-cache true -``` - -The default value may be set by passing an empty value for the option: - -```shell -dokku builder-nixpacks:set node-js-app no-cache -``` - -The `no-cache` property can also be set globally. The global default is `false`, and the global value is used when no app-specific value is set. - -```shell -dokku builder-nixpacks:set --global no-cache true -``` - -The default value may be set by passing an empty value for the option. - -```shell -dokku builder-nixpacks:set --global no-cache +dokku docker-options:add node-js-app build "--no-cache" ``` ### Displaying builder-nixpacks reports for an app @@ -117,23 +99,14 @@ dokku builder-nixpacks:report Builder-nixpacks computed nixpackstoml path: nixpacks2.toml Builder-nixpacks global nixpackstoml path: nixpacks.toml Builder-nixpacks nixpackstoml path: nixpacks2.toml - Builder-nixpacks computed no cache: true - Builder-nixpacks global no cache: false - Builder-nixpacks no cache: true =====> python-sample builder-nixpacks information Builder-nixpacks computed nixpackstoml path: nixpacks.toml Builder-nixpacks global nixpackstoml path: nixpacks.toml Builder-nixpacks nixpackstoml path: - Builder-nixpacks computed no cache: false - Builder-nixpacks global no cache: false - Builder-nixpacks no cache: =====> ruby-sample builder-nixpacks information Builder-nixpacks computed nixpackstoml path: nixpacks.toml Builder-nixpacks global nixpackstoml path: nixpacks.toml Builder-nixpacks nixpackstoml path: - Builder-nixpacks computed no cache: false - Builder-nixpacks global no cache: false - Builder-nixpacks no cache: ``` You can run the command for a specific app also. @@ -147,17 +120,14 @@ dokku builder-nixpacks:report node-js-app Builder-nixpacks computed nixpackstoml path: nixpacks2.toml Builder-nixpacks global nixpackstoml path: nixpacks.toml Builder-nixpacks nixpackstoml path: nixpacks2.toml - Builder-nixpacks computed no cache: true - Builder-nixpacks global no cache: false - Builder-nixpacks no cache: true ``` You can pass flags which will output only the value of the specific information you want. For example: ```shell -dokku builder-nixpacks:report node-js-app --builder-nixpacks-no-cache +dokku builder-nixpacks:report node-js-app --builder-nixpacks-nixpackstoml-path ``` ``` -true +nixpacks2.toml ``` diff --git a/plugins/builder-nixpacks/builder-build b/plugins/builder-nixpacks/builder-build index d1ab7340e..d7fa62820 100755 --- a/plugins/builder-nixpacks/builder-build +++ b/plugins/builder-nixpacks/builder-build @@ -36,7 +36,6 @@ trigger-builder-nixpacks-builder-build() { local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$BUILDER_TYPE") DOCKER_ARGS+=$(: | plugn trigger docker-args-process-build "$APP" "$BUILDER_TYPE") DOCKER_ARGS+=" $DOKKU_GLOBAL_BUILD_ARGS" - [[ "$(fn-builder-nixpacks-computed-no-cache "$APP")" == "true" ]] && DOCKER_ARGS+=" --no-cache" DOCKER_ARGS=" $DOCKER_ARGS " eval set -- "$DOCKER_ARGS" diff --git a/plugins/builder-nixpacks/internal-functions b/plugins/builder-nixpacks/internal-functions index 582fc33fa..d2c60a133 100755 --- a/plugins/builder-nixpacks/internal-functions +++ b/plugins/builder-nixpacks/internal-functions @@ -38,9 +38,6 @@ cmd-builder-nixpacks-report-single() { "--builder-nixpacks-computed-nixpackstoml-path: $(fn-builder-nixpacks-computed-nixpackstoml-path "$APP")" "--builder-nixpacks-global-nixpackstoml-path: $(fn-builder-nixpacks-global-nixpackstoml-path "$APP")" "--builder-nixpacks-nixpackstoml-path: $(fn-builder-nixpacks-nixpackstoml-path "$APP")" - "--builder-nixpacks-computed-no-cache: $(fn-builder-nixpacks-computed-no-cache "$APP")" - "--builder-nixpacks-global-no-cache: $(fn-builder-nixpacks-global-no-cache "$APP")" - "--builder-nixpacks-no-cache: $(fn-builder-nixpacks-no-cache "$APP")" ) if [[ -z "$INFO_FLAG" ]]; then @@ -91,26 +88,3 @@ fn-builder-nixpacks-nixpackstoml-path() { fn-plugin-property-get "builder-nixpacks" "$APP" "nixpackstoml-path" "" } - -fn-builder-nixpacks-computed-no-cache() { - declare APP="$1" - - file="$(fn-builder-nixpacks-no-cache "$APP")" - if [[ "$file" == "" ]]; then - file="$(fn-builder-nixpacks-global-no-cache "$APP")" - fi - - echo "$file" -} - -fn-builder-nixpacks-global-no-cache() { - declare APP="$1" - - fn-plugin-property-get "builder-nixpacks" "--global" "no-cache" "false" -} - -fn-builder-nixpacks-no-cache() { - declare APP="$1" - - fn-plugin-property-get "builder-nixpacks" "$APP" "no-cache" "" -} diff --git a/plugins/builder-nixpacks/subcommands/set b/plugins/builder-nixpacks/subcommands/set index 711b60d72..de7d46513 100755 --- a/plugins/builder-nixpacks/subcommands/set +++ b/plugins/builder-nixpacks/subcommands/set @@ -9,13 +9,13 @@ cmd-builder-nixpacks-set() { declare cmd="builder-nixpacks:set" [[ "$1" == "$cmd" ]] && shift 1 declare APP="$1" KEY="$2" VALUE="$3" - local VALID_KEYS=("nixpackstoml-path" "no-cache") + local VALID_KEYS=("nixpackstoml-path") [[ "$APP" == "--global" ]] || verify_app_name "$APP" [[ -z "$KEY" ]] && dokku_log_fail "No key specified" if ! fn-in-array "$KEY" "${VALID_KEYS[@]}"; then - dokku_log_fail "Invalid key specified, valid keys include: nixpackstoml-path, no-cache" + dokku_log_fail "Invalid key specified, valid keys include: nixpackstoml-path" fi if [[ -n "$VALUE" ]]; then From 364eac374d80c7a3101aab1cf743c66cf06423ee Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 23:30:24 -0500 Subject: [PATCH 084/144] refactor: remove support for the railpack no-cache property This replicates the docker-options functionality. --- docs/deployment/builders/railpack.md | 38 +++------------------ plugins/builder-railpack/builder-build | 1 - plugins/builder-railpack/internal-functions | 26 -------------- plugins/builder-railpack/subcommands/set | 4 +-- 4 files changed, 6 insertions(+), 63 deletions(-) diff --git a/docs/deployment/builders/railpack.md b/docs/deployment/builders/railpack.md index dc0d634fb..cde329846 100644 --- a/docs/deployment/builders/railpack.md +++ b/docs/deployment/builders/railpack.md @@ -89,28 +89,10 @@ dokku builder-railpack:set --global railpackjson-path ### Disabling cache -Cache is enabled by default, but can be disabled by setting the `no-cache` property to `true`: +Disable cache using the [docker-options] plugin: ```shell -dokku builder-railpack:set node-js-app no-cache true -``` - -The default value may be set by passing an empty value for the option: - -```shell -dokku builder-railpack:set node-js-app no-cache -``` - -The `no-cache` property can also be set globally. The global default is `false`, and the global value is used when no app-specific value is set. - -```shell -dokku builder-railpack:set --global no-cache true -``` - -The default value may be set by passing an empty value for the option. - -```shell -dokku builder-railpack:set --global no-cache +dokku docker-options:add node-js-app build "--no-cache" ``` ### Displaying builder-railpack reports for an app @@ -126,23 +108,14 @@ dokku builder-railpack:report Builder-railpack computed railpackjson path: railpack2.json Builder-railpack global railpackjson path: railpack.json Builder-railpack railpackjson path: railpack2.json - Builder-railpack computed no cache: true - Builder-railpack global no cache: false - Builder-railpack no cache: true =====> python-sample builder-railpack information Builder-railpack computed railpackjson path: railpack.json Builder-railpack global railpackjson path: railpack.json Builder-railpack railpackjson path: - Builder-railpack computed no cache: false - Builder-railpack global no cache: false - Builder-railpack no cache: =====> ruby-sample builder-railpack information Builder-railpack computed railpackjson path: railpack.json Builder-railpack global railpackjson path: railpack.json Builder-railpack railpackjson path: - Builder-railpack computed no cache: false - Builder-railpack global no cache: false - Builder-railpack no cache: ``` You can run the command for a specific app also. @@ -156,17 +129,14 @@ dokku builder-railpack:report node-js-app Builder-railpack computed railpackjson path: railpack2.json Builder-railpack global railpackjson path: railpack.json Builder-railpack railpackjson path: railpack2.json - Builder-railpack computed no cache: true - Builder-railpack global no cache: false - Builder-railpack no cache: true ``` You can pass flags which will output only the value of the specific information you want. For example: ```shell -dokku builder-railpack:report node-js-app --builder-railpack-no-cache +dokku builder-railpack:report node-js-app --builder-railpack-railpackjson-path ``` ``` -true +railpack2.json ``` diff --git a/plugins/builder-railpack/builder-build b/plugins/builder-railpack/builder-build index 68d2698f3..afc5ff65f 100755 --- a/plugins/builder-railpack/builder-build +++ b/plugins/builder-railpack/builder-build @@ -35,7 +35,6 @@ trigger-builder-railpack-builder-build() { local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$BUILDER_TYPE") DOCKER_ARGS+=$(: | plugn trigger docker-args-process-build "$APP" "$BUILDER_TYPE") - [[ "$(fn-builder-railpack-computed-no-cache "$APP")" == "true" ]] && DOCKER_ARGS+=" --no-cache" DOCKER_ARGS=" $DOCKER_ARGS " eval set -- "$DOCKER_ARGS" diff --git a/plugins/builder-railpack/internal-functions b/plugins/builder-railpack/internal-functions index a405b2ed0..96dab71bc 100755 --- a/plugins/builder-railpack/internal-functions +++ b/plugins/builder-railpack/internal-functions @@ -38,9 +38,6 @@ cmd-builder-railpack-report-single() { "--builder-railpack-computed-railpackjson-path: $(fn-builder-railpack-computed-railpackjson-path "$APP")" "--builder-railpack-global-railpackjson-path: $(fn-builder-railpack-global-railpackjson-path "$APP")" "--builder-railpack-railpackjson-path: $(fn-builder-railpack-railpackjson-path "$APP")" - "--builder-railpack-computed-no-cache: $(fn-builder-railpack-computed-no-cache "$APP")" - "--builder-railpack-global-no-cache: $(fn-builder-railpack-global-no-cache "$APP")" - "--builder-railpack-no-cache: $(fn-builder-railpack-no-cache "$APP")" ) if [[ -z "$INFO_FLAG" ]]; then @@ -91,26 +88,3 @@ fn-builder-railpack-railpackjson-path() { fn-plugin-property-get "builder-railpack" "$APP" "railpackjson-path" "" } - -fn-builder-railpack-computed-no-cache() { - declare APP="$1" - - file="$(fn-builder-railpack-no-cache "$APP")" - if [[ "$file" == "" ]]; then - file="$(fn-builder-railpack-global-no-cache "$APP")" - fi - - echo "$file" -} - -fn-builder-railpack-global-no-cache() { - declare APP="$1" - - fn-plugin-property-get "builder-railpack" "--global" "no-cache" "false" -} - -fn-builder-railpack-no-cache() { - declare APP="$1" - - fn-plugin-property-get "builder-railpack" "$APP" "no-cache" "" -} diff --git a/plugins/builder-railpack/subcommands/set b/plugins/builder-railpack/subcommands/set index 6001f0c6f..3ac35030d 100755 --- a/plugins/builder-railpack/subcommands/set +++ b/plugins/builder-railpack/subcommands/set @@ -9,13 +9,13 @@ cmd-builder-railpack-set() { declare cmd="builder-railpack:set" [[ "$1" == "$cmd" ]] && shift 1 declare APP="$1" KEY="$2" VALUE="$3" - local VALID_KEYS=("railpackjson-path" "no-cache") + local VALID_KEYS=("railpackjson-path") [[ "$APP" == "--global" ]] || verify_app_name "$APP" [[ -z "$KEY" ]] && dokku_log_fail "No key specified" if ! fn-in-array "$KEY" "${VALID_KEYS[@]}"; then - dokku_log_fail "Invalid key specified, valid keys include: railpackjson-path, no-cache" + dokku_log_fail "Invalid key specified, valid keys include: railpackjson-path" fi if [[ -n "$VALUE" ]]; then From 397c8f6d150dbd241458ba182336e75d30a54764 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 23:31:00 -0500 Subject: [PATCH 085/144] docs: add note on removal of nixpacks no-cache property --- docs/appendices/0.37.0-migration-guide.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/appendices/0.37.0-migration-guide.md b/docs/appendices/0.37.0-migration-guide.md index bc7943389..cb33e72df 100644 --- a/docs/appendices/0.37.0-migration-guide.md +++ b/docs/appendices/0.37.0-migration-guide.md @@ -4,3 +4,4 @@ - Support for the `DOKKU_DOCKERFILE_CACHE_BUILD` environment variable has been removed. Use the `docker-options` plugin to set build arguments for your app instead. - Support for the `DOKKU_DOCKER_BUILD_OPTS` environment variable has been removed. Use the `docker-options` plugin to set build arguments for your app instead. +- Support for the `no-cache` nixpacks property has been removed. Use the `docker-options` plugin to set build arguments for your app instead. From f1763bdaf5082386dfd40271658ad2ea2271aa0b Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sat, 15 Nov 2025 23:35:52 -0500 Subject: [PATCH 086/144] fix: correct lint issue --- plugins/builder-dockerfile/builder-build | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/builder-dockerfile/builder-build b/plugins/builder-dockerfile/builder-build index 56852547c..47da0781f 100755 --- a/plugins/builder-dockerfile/builder-build +++ b/plugins/builder-dockerfile/builder-build @@ -36,7 +36,6 @@ trigger-builder-dockerfile-builder-build() { DOCKER_ARGS+=$(: | plugn trigger docker-args-process-build "$APP" "$BUILDER_TYPE") DOCKER_ARGS+=" $DOKKU_GLOBAL_BUILD_ARGS" - DOCKER_ARGS=" $DOCKER_ARGS " eval set -- "$DOCKER_ARGS" From 3b35656ae9c55f27b1770dc739a54e7b7df809e4 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 16 Nov 2025 00:26:53 -0500 Subject: [PATCH 087/144] fix: shift the unknown arguments out They should just be ignored. --- plugins/builder-dockerfile/builder-build | 1 + plugins/builder-nixpacks/builder-build | 1 + plugins/builder-pack/builder-build | 1 + plugins/builder-railpack/builder-build | 1 + 4 files changed, 4 insertions(+) diff --git a/plugins/builder-dockerfile/builder-build b/plugins/builder-dockerfile/builder-build index 47da0781f..86294c02c 100755 --- a/plugins/builder-dockerfile/builder-build +++ b/plugins/builder-dockerfile/builder-build @@ -236,6 +236,7 @@ trigger-builder-dockerfile-builder-build() { shift 1 ;; *) + shift continue ;; esac diff --git a/plugins/builder-nixpacks/builder-build b/plugins/builder-nixpacks/builder-build index d7fa62820..beecc3a1a 100755 --- a/plugins/builder-nixpacks/builder-build +++ b/plugins/builder-nixpacks/builder-build @@ -158,6 +158,7 @@ trigger-builder-nixpacks-builder-build() { shift 1 ;; *) + shift continue ;; esac diff --git a/plugins/builder-pack/builder-build b/plugins/builder-pack/builder-build index e3d3eef84..50daad571 100755 --- a/plugins/builder-pack/builder-build +++ b/plugins/builder-pack/builder-build @@ -123,6 +123,7 @@ trigger-builder-pack-builder-build() { shift 1 ;; *) + shift continue ;; esac diff --git a/plugins/builder-railpack/builder-build b/plugins/builder-railpack/builder-build index afc5ff65f..133de71db 100755 --- a/plugins/builder-railpack/builder-build +++ b/plugins/builder-railpack/builder-build @@ -114,6 +114,7 @@ trigger-builder-railpack-builder-build() { shift 1 ;; *) + shift continue ;; esac From 9bda9e721fec7a2bebb42ed2cd7ebc5ad2b50ce3 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 16 Nov 2025 00:30:35 -0500 Subject: [PATCH 088/144] feat: add support for flags with = signs --- plugins/builder-nixpacks/builder-build | 78 ++++++++++++- plugins/builder-pack/builder-build | 153 ++++++++++++++++++++++++- 2 files changed, 226 insertions(+), 5 deletions(-) diff --git a/plugins/builder-nixpacks/builder-build b/plugins/builder-nixpacks/builder-build index beecc3a1a..7a5a5fc08 100755 --- a/plugins/builder-nixpacks/builder-build +++ b/plugins/builder-nixpacks/builder-build @@ -147,8 +147,82 @@ trigger-builder-nixpacks-builder-build() { fi shift 1 ;; - --platform | --cache-key | --incremental-cache-image | --libs | --cache-from | --docker-host | --add-host | --docker-tls-verify) - # todo: additionally support --flag=value syntax + --platform) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --platform=*) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --cache-key) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --cache-key=*) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --incremental-cache-image) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --incremental-cache-image=*) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --libs) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --libs=*) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --cache-from) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --cache-from=*) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --docker-host) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --docker-host=*) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --add-host) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --add-host=*) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --docker-tls-verify) + NIXPACKS_ARGS+=("$1") + NIXPACKS_ARGS+=("$2") + shift 2 + ;; + --docker-tls-verify=*) NIXPACKS_ARGS+=("$1") NIXPACKS_ARGS+=("$2") shift 2 diff --git a/plugins/builder-pack/builder-build b/plugins/builder-pack/builder-build index 50daad571..338d8144b 100755 --- a/plugins/builder-pack/builder-build +++ b/plugins/builder-pack/builder-build @@ -112,12 +112,159 @@ trigger-builder-pack-builder-build() { fi shift 1 ;; - --cache | --cache-image | --creation-time | --env-file | --extension | --gid | --insecure-registry | --lifecycle-image | --network | --platform | --post-buildpack | --pre-buildpack | --previous-image | --pull-policy | --run-image | --uid | --workspace) - # todo: additionally support --flag=value syntax - PACK_ARGS+=("$1") + --cache) + PACK_ARGS+=("--cache") PACK_ARGS+=("$2") shift 2 ;; + --cache=*) + PACK_ARGS+=("--cache" "${1#--cache=}") + shift 1 + ;; + --cache-image) + PACK_ARGS+=("--cache-image") + PACK_ARGS+=("$2") + shift 2 + ;; + --cache-image=*) + PACK_ARGS+=("--cache-image" "${1#--cache-image=}") + shift 1 + ;; + --creation-time) + PACK_ARGS+=("--creation-time") + PACK_ARGS+=("$2") + shift 2 + ;; + --creation-time=*) + PACK_ARGS+=("--creation-time" "${1#--creation-time=}") + shift 1 + ;; + --env-file) + PACK_ARGS+=("--env-file") + PACK_ARGS+=("$2") + shift 2 + ;; + --env-file=*) + PACK_ARGS+=("--env-file" "${1#--env-file=}") + shift 1 + ;; + --extension) + PACK_ARGS+=("--extension") + PACK_ARGS+=("$2") + shift 2 + ;; + --extension=*) + PACK_ARGS+=("--extension" "${1#--extension=}") + shift 1 + ;; + --gid) + PACK_ARGS+=("--gid") + PACK_ARGS+=("$2") + shift 2 + ;; + --gid=*) + PACK_ARGS+=("--gid" "${1#--gid=}") + shift 1 + ;; + --insecure-registry) + PACK_ARGS+=("--insecure-registry") + PACK_ARGS+=("$2") + shift 2 + ;; + --insecure-registry=*) + PACK_ARGS+=("--insecure-registry" "${1#--insecure-registry=}") + shift 1 + ;; + --lifecycle-image) + PACK_ARGS+=("--lifecycle-image") + PACK_ARGS+=("$2") + shift 2 + ;; + --lifecycle-image=*) + PACK_ARGS+=("--lifecycle-image" "${1#--lifecycle-image=}") + shift 1 + ;; + --network) + PACK_ARGS+=("--network") + PACK_ARGS+=("$2") + shift 2 + ;; + --network=*) + PACK_ARGS+=("--network" "${1#--network=}") + shift 1 + ;; + --platform) + PACK_ARGS+=("--platform") + PACK_ARGS+=("$2") + shift 2 + ;; + --platform=*) + PACK_ARGS+=("--platform" "${1#--platform=}") + shift 1 + ;; + --post-buildpack) + PACK_ARGS+=("--post-buildpack") + PACK_ARGS+=("$2") + shift 2 + ;; + --post-buildpack=*) + PACK_ARGS+=("--post-buildpack" "${1#--post-buildpack=}") + shift 1 + ;; + --pre-buildpack) + PACK_ARGS+=("--pre-buildpack") + PACK_ARGS+=("$2") + shift 2 + ;; + --pre-buildpack=*) + PACK_ARGS+=("--pre-buildpack" "${1#--pre-buildpack=}") + shift 1 + ;; + --previous-image) + PACK_ARGS+=("--previous-image") + PACK_ARGS+=("$2") + shift 2 + ;; + --previous-image=*) + PACK_ARGS+=("--previous-image" "${1#--previous-image=}") + shift 1 + ;; + --pull-policy) + PACK_ARGS+=("--pull-policy") + PACK_ARGS+=("$2") + shift 2 + ;; + --pull-policy=*) + PACK_ARGS+=("--pull-policy" "${1#--pull-policy=}") + shift 1 + ;; + --run-image) + PACK_ARGS+=("--run-image") + PACK_ARGS+=("$2") + shift 2 + ;; + --run-image=*) + PACK_ARGS+=("--run-image" "${1#--run-image=}") + shift 1 + ;; + --uid) + PACK_ARGS+=("--uid") + PACK_ARGS+=("$2") + shift 2 + ;; + --uid=*) + PACK_ARGS+=("--uid" "${1#--uid=}") + shift 1 + ;; + --workspace) + PACK_ARGS+=("--workspace") + PACK_ARGS+=("$2") + shift 2 + ;; + --workspace=*) + PACK_ARGS+=("--workspace" "${1#--workspace=}") + shift 1 + ;; --clear-cache | --disable-system-buildpacks | --publish | --trust-builder | --trust-extra-buildpacks | --userns-host) PACK_ARGS+=("$1") shift 1 From 406a82de29ecd5994a351ccdf82912970ec1bf4d Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 16 Nov 2025 01:10:15 -0500 Subject: [PATCH 089/144] fix: use call instead of env --- plugins/builder-dockerfile/builder-build | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/builder-dockerfile/builder-build b/plugins/builder-dockerfile/builder-build index 86294c02c..68c7e8399 100755 --- a/plugins/builder-dockerfile/builder-build +++ b/plugins/builder-dockerfile/builder-build @@ -115,12 +115,12 @@ trigger-builder-dockerfile-builder-build() { shift 1 ;; --call) - DOCKERFILE_ARGS+=("--env") + DOCKERFILE_ARGS+=("--call") DOCKERFILE_ARGS+=("$2") shift 2 ;; - --env=*) - DOCKERFILE_ARGS+=("--env" "${1#--env=}") + --call=*) + DOCKERFILE_ARGS+=("--call" "${1#--call=}") shift 1 ;; --cgroup-parent) From 9411c4ddc1f1b785ac8e930bf7579d3292cd55f5 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 16 Nov 2025 01:10:53 -0500 Subject: [PATCH 090/144] test: ensure the deploy succeeds and then check the output vs using grep --- tests/unit/config.bats | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/unit/config.bats b/tests/unit/config.bats index 1825feb12..e9bfb1fce 100644 --- a/tests/unit/config.bats +++ b/tests/unit/config.bats @@ -486,10 +486,15 @@ teardown() { @test "(config) global config (dockerfile)" { run deploy_app dockerfile - run /bin/bash -c "dokku run $TEST_APP env | grep -E '^global_test=true'" echo "output: $output" echo "status: $status" assert_success + + run /bin/bash -c "dokku run $TEST_APP env" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "global_test=true" } @test "(config) config:show" { From 531bb205192661c3c8c17ed154222adf11e8827f Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 16 Nov 2025 01:11:09 -0500 Subject: [PATCH 091/144] tests: respect the globally specified default branch when running tests --- tests/test_deploy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_deploy b/tests/test_deploy index 01ecbda18..651e16098 100755 --- a/tests/test_deploy +++ b/tests/test_deploy @@ -45,10 +45,11 @@ git add . [[ -x pre-commit ]] && ./pre-commit "$REMOTE" "$REPO" git commit -m 'initial commit' +current_branch="$(git rev-parse --abbrev-ref HEAD)" if [[ "$SHOULD_FAIL" == true ]]; then - git push target "master:$BRANCH" && succeeded git-push + git push target "$current_branch:$BRANCH" && succeeded git-push else - git push target "master:$BRANCH" || failed git-push + git push target "$current_branch:$BRANCH" || failed git-push fi if [[ -x post-deploy ]]; then ./post-deploy "$REMOTE" "$REPO" || failed post-deploy From a9127e3abd5929206e134566567eb74b3482e247 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 16 Nov 2025 01:24:28 -0500 Subject: [PATCH 092/144] fix: correctly handle exiting the argument parsing loop --- plugins/builder-dockerfile/builder-build | 7 +++++-- plugins/builder-nixpacks/builder-build | 7 +++++-- plugins/builder-pack/builder-build | 7 +++++-- plugins/builder-railpack/builder-build | 7 +++++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/plugins/builder-dockerfile/builder-build b/plugins/builder-dockerfile/builder-build index 68c7e8399..f8c8c46fa 100755 --- a/plugins/builder-dockerfile/builder-build +++ b/plugins/builder-dockerfile/builder-build @@ -236,8 +236,11 @@ trigger-builder-dockerfile-builder-build() { shift 1 ;; *) - shift - continue + if [[ -n "$1" ]]; then + shift + continue + fi + break ;; esac done diff --git a/plugins/builder-nixpacks/builder-build b/plugins/builder-nixpacks/builder-build index 7a5a5fc08..d74d9ba3a 100755 --- a/plugins/builder-nixpacks/builder-build +++ b/plugins/builder-nixpacks/builder-build @@ -232,8 +232,11 @@ trigger-builder-nixpacks-builder-build() { shift 1 ;; *) - shift - continue + if [[ -n "$1" ]]; then + shift + continue + fi + break ;; esac done diff --git a/plugins/builder-pack/builder-build b/plugins/builder-pack/builder-build index 338d8144b..305d21514 100755 --- a/plugins/builder-pack/builder-build +++ b/plugins/builder-pack/builder-build @@ -270,8 +270,11 @@ trigger-builder-pack-builder-build() { shift 1 ;; *) - shift - continue + if [[ -n "$1" ]]; then + shift + continue + fi + break ;; esac done diff --git a/plugins/builder-railpack/builder-build b/plugins/builder-railpack/builder-build index 133de71db..3b7e7e6df 100755 --- a/plugins/builder-railpack/builder-build +++ b/plugins/builder-railpack/builder-build @@ -114,8 +114,11 @@ trigger-builder-railpack-builder-build() { shift 1 ;; *) - shift - continue + if [[ -n "$1" ]]; then + shift + continue + fi + break ;; esac done From 4da1fea274b0034049bf7b7e0ce8f67067db0c6c Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 16 Nov 2025 01:45:11 -0500 Subject: [PATCH 093/144] fix: use correct check for BUILDPACK_URL Also fix lint issue --- plugins/builder-pack/builder-build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/builder-pack/builder-build b/plugins/builder-pack/builder-build index 192ebb33c..ceb6380e1 100755 --- a/plugins/builder-pack/builder-build +++ b/plugins/builder-pack/builder-build @@ -65,7 +65,7 @@ trigger-builder-pack-builder-build() { esac done - if [ -z "$BUILDPACK_URL" ]; then + if [ -n "$BUILDPACK_URL" ]; then PACK_ARGS+=("--builder") PACK_ARGS+=("$BUILDPACK_URL") elif [ -f "$SOURCECODE_WORK_DIR/.buildpacks" ]; then @@ -76,7 +76,7 @@ trigger-builder-pack-builder-build() { PACK_ARGS+=("--builder") PACK_ARGS+=("$buildpack") - done < "$SOURCECODE_WORK_DIR/.buildpacks" + done <"$SOURCECODE_WORK_DIR/.buildpacks" fi pack build "$IMAGE" --builder "$DOKKU_CNB_BUILDER" --path "$SOURCECODE_WORK_DIR" --default-process web "${PACK_ARGS[@]}" "${ENV_ARGS[@]}" From 4eafd3b5b772379e38c1d3b3db0730673c7cd173 Mon Sep 17 00:00:00 2001 From: dragonhunt02 Date: Sat, 22 Mar 2025 22:01:00 +0000 Subject: [PATCH 094/144] feat: implement proxy container :label functionality --- plugins/caddy-vhosts/command-functions | 11 +++ .../caddy-vhosts/docker-args-process-deploy | 10 +++ plugins/caddy-vhosts/help-functions | 1 + plugins/caddy-vhosts/subcommands/label | 6 ++ plugins/haproxy-vhosts/command-functions | 11 +++ .../haproxy-vhosts/docker-args-process-deploy | 10 +++ plugins/haproxy-vhosts/help-functions | 1 + plugins/haproxy-vhosts/subcommands/label | 6 ++ plugins/openresty-vhosts/command-functions | 11 +++ .../docker-args-process-deploy | 10 +++ plugins/openresty-vhosts/help-functions | 1 + plugins/openresty-vhosts/subcommands/label | 6 ++ plugins/proxy/command-functions | 84 +++++++++++++++++++ plugins/proxy/functions | 77 +++++++++++++++++ plugins/traefik-vhosts/command-functions | 11 +++ .../traefik-vhosts/docker-args-process-deploy | 10 +++ plugins/traefik-vhosts/help-functions | 1 + plugins/traefik-vhosts/subcommands/label | 6 ++ 18 files changed, 273 insertions(+) create mode 100755 plugins/caddy-vhosts/subcommands/label create mode 100755 plugins/haproxy-vhosts/subcommands/label create mode 100755 plugins/openresty-vhosts/subcommands/label create mode 100755 plugins/proxy/command-functions create mode 100755 plugins/proxy/functions create mode 100755 plugins/traefik-vhosts/subcommands/label diff --git a/plugins/caddy-vhosts/command-functions b/plugins/caddy-vhosts/command-functions index 1f04357ff..e515e2820 100755 --- a/plugins/caddy-vhosts/command-functions +++ b/plugins/caddy-vhosts/command-functions @@ -1,9 +1,20 @@ #!/usr/bin/env bash source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/proxy/command-functions" source "$PLUGIN_AVAILABLE_PATH/caddy-vhosts/internal-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x +cmd-caddy-label() { + declare desc="set/unset/show container proxy labels" + declare cmd="caddy:label" + [[ "$1" == "$cmd" ]] && shift 1 + declare APP="$1" + + set -- caddy "$@" + cmd-proxy-label "$@" +} + cmd-caddy-report() { declare desc="displays a caddy report for one or more apps" declare cmd="caddy:report" diff --git a/plugins/caddy-vhosts/docker-args-process-deploy b/plugins/caddy-vhosts/docker-args-process-deploy index 9dd1f61f1..1be3b7f3a 100755 --- a/plugins/caddy-vhosts/docker-args-process-deploy +++ b/plugins/caddy-vhosts/docker-args-process-deploy @@ -1,5 +1,6 @@ #!/usr/bin/env bash source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/proxy/functions" source "$PLUGIN_AVAILABLE_PATH/caddy-vhosts/internal-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x @@ -129,6 +130,15 @@ trigger-caddy-vhosts-docker-args-process-deploy() { fi fi + local proxy_labels_file_path=$(fn-proxy-get-labels-file-path "$APP" caddy) + fn-proxy-report-label-overwrite "$output" "$proxy_labels_file_path" + + if [[ -f "$proxy_labels_file_path" ]]; then + local user_proxy_labels="$(<"$proxy_labels_file_path")" + user_proxy_labels="$(echo "$user_proxy_labels" | tr '\r\n' ' ')" + output="$output $user_proxy_labels" + fi + echo -n "$STDIN$output" } diff --git a/plugins/caddy-vhosts/help-functions b/plugins/caddy-vhosts/help-functions index 287ffed04..76b161ef3 100755 --- a/plugins/caddy-vhosts/help-functions +++ b/plugins/caddy-vhosts/help-functions @@ -30,6 +30,7 @@ fn-help-content() { caddy:report [] [], Displays an caddy report for one or more apps caddy:set (), Set or clear an caddy property for an app caddy:show-config , Display caddy compose config + caddy:label (), Show, set or clear a caddy label for an app caddy:start, Starts the caddy server caddy:stop, Stops the caddy server help_content diff --git a/plugins/caddy-vhosts/subcommands/label b/plugins/caddy-vhosts/subcommands/label new file mode 100755 index 000000000..2a5e0421b --- /dev/null +++ b/plugins/caddy-vhosts/subcommands/label @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/caddy-vhosts/command-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-caddy-label "$@" diff --git a/plugins/haproxy-vhosts/command-functions b/plugins/haproxy-vhosts/command-functions index 481a6a682..f7ce1be91 100755 --- a/plugins/haproxy-vhosts/command-functions +++ b/plugins/haproxy-vhosts/command-functions @@ -1,9 +1,20 @@ #!/usr/bin/env bash source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/proxy/command-functions" source "$PLUGIN_AVAILABLE_PATH/haproxy-vhosts/internal-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x +cmd-haproxy-label() { + declare desc="set/unset/show container proxy labels" + declare cmd="haproxy:label" + [[ "$1" == "$cmd" ]] && shift 1 + declare APP="$1" + + set -- haproxy "$@" + cmd-proxy-label "$@" +} + cmd-haproxy-report() { declare desc="displays a haproxy report for one or more apps" declare cmd="haproxy:report" diff --git a/plugins/haproxy-vhosts/docker-args-process-deploy b/plugins/haproxy-vhosts/docker-args-process-deploy index 60577f9cf..e60931fc7 100755 --- a/plugins/haproxy-vhosts/docker-args-process-deploy +++ b/plugins/haproxy-vhosts/docker-args-process-deploy @@ -1,5 +1,6 @@ #!/usr/bin/env bash source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/proxy/functions" source "$PLUGIN_AVAILABLE_PATH/haproxy-vhosts/internal-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x @@ -123,6 +124,15 @@ trigger-haproxy-vhosts-docker-args-process-deploy() { fi fi + local proxy_labels_file_path=$(fn-proxy-get-labels-file-path "$APP" haproxy) + fn-proxy-report-label-overwrite "$output" "$proxy_labels_file_path" + + if [[ -f "$proxy_labels_file_path" ]]; then + local user_proxy_labels="$(<"$proxy_labels_file_path")" + user_proxy_labels="$(echo "$user_proxy_labels" | tr '\r\n' ' ')" + output="$output $user_proxy_labels" + fi + echo -n "$STDIN$output" } diff --git a/plugins/haproxy-vhosts/help-functions b/plugins/haproxy-vhosts/help-functions index b28fd2b1f..1c7f86674 100755 --- a/plugins/haproxy-vhosts/help-functions +++ b/plugins/haproxy-vhosts/help-functions @@ -30,6 +30,7 @@ fn-help-content() { haproxy:report [] [], Displays an haproxy report for one or more apps haproxy:set (), Set or clear an haproxy property for an app haproxy:show-config , Display haproxy compose config + haproxy:label (), Show, set or clear a haproxy label for an app haproxy:start, Starts the haproxy server haproxy:stop, Stops the haproxy server help_content diff --git a/plugins/haproxy-vhosts/subcommands/label b/plugins/haproxy-vhosts/subcommands/label new file mode 100755 index 000000000..d52aa7b16 --- /dev/null +++ b/plugins/haproxy-vhosts/subcommands/label @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/haproxy-vhosts/command-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-haproxy-label "$@" diff --git a/plugins/openresty-vhosts/command-functions b/plugins/openresty-vhosts/command-functions index 926839ff4..d9f6401c7 100755 --- a/plugins/openresty-vhosts/command-functions +++ b/plugins/openresty-vhosts/command-functions @@ -1,9 +1,20 @@ #!/usr/bin/env bash source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/proxy/command-functions" source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/internal-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x +cmd-openresty-label() { + declare desc="set/unset/show container proxy labels" + declare cmd="openresty:label" + [[ "$1" == "$cmd" ]] && shift 1 + declare APP="$1" + + set -- openresty "$@" + cmd-proxy-label "$@" +} + cmd-openresty-report() { declare desc="displays a openresty report for one or more apps" declare cmd="openresty:report" diff --git a/plugins/openresty-vhosts/docker-args-process-deploy b/plugins/openresty-vhosts/docker-args-process-deploy index beefa73e3..6c116b2e2 100755 --- a/plugins/openresty-vhosts/docker-args-process-deploy +++ b/plugins/openresty-vhosts/docker-args-process-deploy @@ -2,6 +2,7 @@ set -eo pipefail [[ $DOKKU_TRACE ]] && set -x source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/proxy/functions" source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/internal-functions" trigger-openresty-vhosts-docker-args-process-deploy() { @@ -181,6 +182,15 @@ trigger-openresty-vhosts-docker-args-process-deploy() { value="$(fn-openresty-x-forwarded-ssl "$APP")" [[ -n "$value" ]] && output="$output '--label=openresty.x-forwarded-ssl=$value'" + local proxy_labels_file_path=$(fn-proxy-get-labels-file-path "$APP" openresty) + fn-proxy-report-label-overwrite "$output" "$proxy_labels_file_path" + + if [[ -f "$proxy_labels_file_path" ]]; then + local user_proxy_labels="$(<"$proxy_labels_file_path")" + user_proxy_labels="$(echo "$user_proxy_labels" | tr '\r\n' ' ')" + output="$output $user_proxy_labels" + fi + echo -n "$STDIN$output" } diff --git a/plugins/openresty-vhosts/help-functions b/plugins/openresty-vhosts/help-functions index d8fc19e7a..d8a8414fc 100755 --- a/plugins/openresty-vhosts/help-functions +++ b/plugins/openresty-vhosts/help-functions @@ -30,6 +30,7 @@ fn-help-content() { openresty:report [] [], Displays an openresty report for one or more apps openresty:set (), Set or clear an openresty property for an app openresty:show-config , Display openresty compose config + openresty:label (), Show, set or clear an openresty label for an app openresty:start, Starts the openresty server openresty:stop, Stops the openresty server help_content diff --git a/plugins/openresty-vhosts/subcommands/label b/plugins/openresty-vhosts/subcommands/label new file mode 100755 index 000000000..b1a49be45 --- /dev/null +++ b/plugins/openresty-vhosts/subcommands/label @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/command-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-openresty-label "$@" diff --git a/plugins/proxy/command-functions b/plugins/proxy/command-functions new file mode 100755 index 000000000..d38158421 --- /dev/null +++ b/plugins/proxy/command-functions @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/proxy/functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-proxy-label-show() { + declare desc="show a proxy label for an app" + declare cmd="proxy:label:show" + [[ "$1" == "$cmd" ]] && shift 1 + declare PROXY="$1" APP="$2" PASSED_LABEL_NAME="$3" + shift 2 + + if [[ -n "$PASSED_LABEL_NAME" ]]; then + fn-proxy-show-label "$PROXY" "$APP" "$PASSED_LABEL_NAME" + return + fi + + fn-proxy-display-labels "$PROXY" "$APP" + return 0 +} + +cmd-proxy-label-add() { + declare desc="adds container label to app for specified proxy" + declare PROXY="$1" APP="$2" PASSED_LABEL_NAME="$3" PASSED_LABEL_VALUE="$4" + if [[ -z "$PASSED_LABEL_NAME" ]]; then + dokku_log_fail "Please specify a container label for the app" + fi + + if [[ -z "$PASSED_LABEL_VALUE" ]]; then + dokku_log_fail "Please specify a value for the container label" + fi + + local proxy_labels_file_path="$(fn-proxy-get-labels-file-path "$PROXY" "$APP")" + touch "$proxy_labels_file_path" + cmd-proxy-label-remove "$PROXY" "$APP" "$PASSED_LABEL_NAME" >/dev/null + echo "--label '${PASSED_LABEL_NAME}=${PASSED_LABEL_VALUE}' " >>"$proxy_labels_file_path" + local all_proxy_labels="$(<"$proxy_labels_file_path")" + echo -e "${all_proxy_labels}" | sed '/^$/d' | sort -u >"$proxy_labels_file_path" +} + +cmd-proxy-label-remove() { + declare desc="removes container label from app for specified proxy" + declare PROXY="$1" APP="$2" PASSED_LABEL_NAME="$3" + + if [[ -z "$PASSED_LABEL_NAME" ]]; then + dokku_log_fail "Please specify a container label for the app" + fi + + local proxy_labels_file_path="$(fn-proxy-get-labels-file-path "$PROXY" "$APP")" + if [[ ! -s "$proxy_labels_file_path" ]]; then + return + fi + + local all_proxy_labels="$(<"$proxy_labels_file_path")" + local escaped_label_name="$(fn-proxy-escape-extended-sed "$PASSED_LABEL_NAME")" + local all_proxy_labels="$(echo -e "${all_proxy_labels}" | sed -E "s/^--label ['\"]${escaped_label_name}=.*\$//")" + echo -e "${all_proxy_labels}" | sed '/^$/d' | sort -u >"$proxy_labels_file_path" +} + +cmd-proxy-label() { + declare desc="manage proxy labels for an app" + declare cmd="proxy:label" + shift 1 # the command comes from the upstream plugin + declare PROXY="$1" ACTION="$2" APP="$3" PASSED_LABEL_NAME="$4" PASSED_LABEL_VALUE="$5" + + [[ -z "$PROXY" ]] && dokku_log_fail "Error invoking proxy:label command" + verify_app_name "$APP" + + if [[ -n "$PASSED_LABEL_NAME" ]] && [[ ! "$PASSED_LABEL_NAME" =~ ^$PROXY ]]; then + dokku_log_fail "Please specify a valid $PROXY label name for the app" + fi + + if [[ "$action" == "show" ]]; then + cmd-proxy-label-show "$PROXY" "$APP" "$PASSED_LABEL_NAME" + return 0 + elif [[ "$ACTION" == "set" ]]; then + cmd-proxy-label-add "$PROXY" "$APP" "$PASSED_LABEL_NAME" "$PASSED_LABEL_VALUE" + elif [[ "$action" == "unset" ]]; then + cmd-proxy-label-remove "$PROXY" "$APP" "$PASSED_LABEL_NAME" + else + dokku_log_fail "Please specify a valid action ('set', 'unset' or 'show')" + fi +} diff --git a/plugins/proxy/functions b/plugins/proxy/functions new file mode 100755 index 000000000..7a83e9677 --- /dev/null +++ b/plugins/proxy/functions @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" + +fn-proxy-escape-extended-sed() { + # Escaped delimiter is '/' + local input="$1" + echo "$input" | sed -E 's#[][()\.^$?*+{}|/]#\\&#g' +} + +fn-proxy-report-label-overwrite() { + declare desc="overwrite proxy labels file with new labels" + declare LABELS="$1" PROXY_LABELS_FILE_PATH="$2" + + if [[ -f "$PROXY_LABELS_FILE_PATH" ]]; then + local DONE=false + until $DONE; do + local line + read -r line || local DONE=true + + [[ -z "$line" ]] && continue + + case "$line" in + \#*) + continue + ;; + + --label*) + local label_name="$(echo "$line" | sed -E "s/^--label ['\"]([^=]+)=.*/\1/")" + local escaped_label_name="$(fn-proxy-escape-extended-sed "$passed_label_name")" + local match="$(echo "$LABELS" | sed -E "s/.*--label ['\"]($escaped_label_name)=.*/\1/")" + if [[ -n "$match" ]]; then + dokku_log_warn "Dokku label \"$match\" will be overwritten by user-set proxy label." + fi + continue + ;; + + *) + dokku_log_warn "Invalid line '$line' in proxy label file $PROXY_FILE_PATH" + ;; + esac + done <"$PROXY_LABELS_FILE_PATH" + fi +} + +fn-proxy-get-labels-file-path() { + declare desc="return proxy labels config file path for specified proxy" + declare PROXY="$1" APP="$2" + local PROXY_FILE="${DOKKU_ROOT}/${APP}/PROXY_LABELS_${PROXY^^}" + + echo "$PROXY_FILE" +} + +fn-proxy-display-labels() { + declare desc="print user-set app container labels for specified proxy" + declare PROXY="$1" APP="$2" + local proxy_labels_file_path="$(fn-proxy-get-labels-file-path "$PROXY" "$APP")" + echo "${PROXY^} labels:" + sed -e 's/^/ /' "$proxy_labels_file_path" +} + +fn-proxy-show-label() { + declare desc="print single user-set app container label for specified proxy" + declare PROXY="$1" APP="$2" PASSED_LABEL_NAME="$3" + + local proxy_labels_file_path="$(fn-proxy-get-labels-file-path "$PROXY" "$APP")" + if [[ ! -s "$proxy_labels_file_path" ]]; then + return + fi + + local all_proxy_labels="$(<"$proxy_labels_file_path")" + local escaped_label_name="$(fn-proxy-escape-extended-sed "$passed_label_name")" + local proxy_label_value="$(echo -e "${all_proxy_labels}" | sed -E -n "s/^--label ['\"]${escaped_label_name}=(.*)['\"] \$/\1/p")" + + echo "$proxy_label_value" +} diff --git a/plugins/traefik-vhosts/command-functions b/plugins/traefik-vhosts/command-functions index 93f5429d5..5a9eb91ec 100755 --- a/plugins/traefik-vhosts/command-functions +++ b/plugins/traefik-vhosts/command-functions @@ -1,9 +1,20 @@ #!/usr/bin/env bash source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/proxy/command-functions" source "$PLUGIN_AVAILABLE_PATH/traefik-vhosts/internal-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x +cmd-traefik-label() { + declare desc="set/unset/show container proxy labels" + declare cmd="traefik:label" + [[ "$1" == "$cmd" ]] && shift 1 + declare APP="$1" + + set -- traefik "$@" + cmd-proxy-label "$@" +} + cmd-traefik-report() { declare desc="displays a traefik report for one or more apps" declare cmd="traefik:report" diff --git a/plugins/traefik-vhosts/docker-args-process-deploy b/plugins/traefik-vhosts/docker-args-process-deploy index dfb8f075f..ea01d0c33 100755 --- a/plugins/traefik-vhosts/docker-args-process-deploy +++ b/plugins/traefik-vhosts/docker-args-process-deploy @@ -1,5 +1,6 @@ #!/usr/bin/env bash source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/proxy/functions" source "$PLUGIN_AVAILABLE_PATH/traefik-vhosts/internal-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x @@ -130,6 +131,15 @@ trigger-traefik-vhosts-docker-args-process-deploy() { fi fi + local proxy_labels_file_path=$(fn-proxy-get-labels-file-path "$APP" traefik) + fn-proxy-report-label-overwrite "$output" "$proxy_labels_file_path" + + if [[ -f "$proxy_labels_file_path" ]]; then + local user_proxy_labels="$(<"$proxy_labels_file_path")" + user_proxy_labels="$(echo "$user_proxy_labels" | tr '\r\n' ' ')" + output="$output $user_proxy_labels" + fi + echo -n "$STDIN$output" } diff --git a/plugins/traefik-vhosts/help-functions b/plugins/traefik-vhosts/help-functions index 22ec4c1a8..3ceb61000 100755 --- a/plugins/traefik-vhosts/help-functions +++ b/plugins/traefik-vhosts/help-functions @@ -30,6 +30,7 @@ fn-help-content() { traefik:report [] [], Displays an traefik report for one or more apps traefik:set (), Set or clear an traefik property for an app traefik:show-config , Display traefik compose config + traefik:label (), Show, set or clear a traefik label for an app traefik:start, Starts the traefik server traefik:stop, Stops the traefik server help_content diff --git a/plugins/traefik-vhosts/subcommands/label b/plugins/traefik-vhosts/subcommands/label new file mode 100755 index 000000000..719f47b8b --- /dev/null +++ b/plugins/traefik-vhosts/subcommands/label @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/traefik-vhosts/command-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-traefik-label "$@" From 12cc31427b4a3bfb7d8f47ecebad0735cd26018f Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 16 Nov 2025 16:30:56 -0500 Subject: [PATCH 095/144] refactor: split out label subcommands into distinct commands --- plugins/caddy-vhosts/command-functions | 26 +++++++++--- plugins/caddy-vhosts/commands | 13 ++++++ plugins/caddy-vhosts/subcommands/label | 6 --- plugins/haproxy-vhosts/command-functions | 26 +++++++++--- plugins/haproxy-vhosts/commands | 13 ++++++ plugins/haproxy-vhosts/subcommands/label | 6 --- plugins/openresty-vhosts/command-functions | 26 +++++++++--- plugins/openresty-vhosts/commands | 13 ++++++ plugins/openresty-vhosts/subcommands/label | 6 --- plugins/proxy/command-functions | 49 +++++++--------------- plugins/traefik-vhosts/command-functions | 26 +++++++++--- plugins/traefik-vhosts/commands | 13 ++++++ plugins/traefik-vhosts/subcommands/label | 6 --- 13 files changed, 146 insertions(+), 83 deletions(-) delete mode 100755 plugins/caddy-vhosts/subcommands/label delete mode 100755 plugins/haproxy-vhosts/subcommands/label delete mode 100755 plugins/openresty-vhosts/subcommands/label delete mode 100755 plugins/traefik-vhosts/subcommands/label diff --git a/plugins/caddy-vhosts/command-functions b/plugins/caddy-vhosts/command-functions index e515e2820..c2ec92404 100755 --- a/plugins/caddy-vhosts/command-functions +++ b/plugins/caddy-vhosts/command-functions @@ -5,14 +5,28 @@ source "$PLUGIN_AVAILABLE_PATH/caddy-vhosts/internal-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x -cmd-caddy-label() { - declare desc="set/unset/show container proxy labels" - declare cmd="caddy:label" +cmd-caddy-label-add() { + declare desc="adds container label to app for specified proxy" + declare cmd="caddy:label:add" [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - set -- caddy "$@" - cmd-proxy-label "$@" + cmd-proxy-label-add "proxy:label:add" "caddy" "$@" +} + +cmd-caddy-label-remove() { + declare desc="removes container label from app for specified proxy" + declare cmd="caddy:label:remove" + [[ "$1" == "$cmd" ]] && shift 1 + + cmd-proxy-label-remove "proxy:label:remove" "caddy" "$@" +} + +cmd-caddy-label-show() { + declare desc="shows container labels for app for specified proxy" + declare cmd="caddy:label:show" + [[ "$1" == "$cmd" ]] && shift 1 + + cmd-proxy-label-show "proxy:label:show" "caddy" "$@" } cmd-caddy-report() { diff --git a/plugins/caddy-vhosts/commands b/plugins/caddy-vhosts/commands index 03e69b312..76a267f5a 100755 --- a/plugins/caddy-vhosts/commands +++ b/plugins/caddy-vhosts/commands @@ -1,6 +1,7 @@ #!/usr/bin/env bash [[ " caddy:help help " == *" $1 "* ]] || exit "$DOKKU_NOT_IMPLEMENTED_EXIT" source "$PLUGIN_AVAILABLE_PATH/caddy-vhosts/help-functions" +source "$PLUGIN_AVAILABLE_PATH/caddy-vhosts/command-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x @@ -9,6 +10,18 @@ case "$1" in cmd-caddy-help "$@" ;; + caddy:label:add) + cmd-caddy-label-add "$@" + ;; + + caddy:label:remove) + cmd-caddy-label-remove "$@" + ;; + + caddy:label:show) + cmd-caddy-label-show "$@" + ;; + *) exit "$DOKKU_NOT_IMPLEMENTED_EXIT" ;; diff --git a/plugins/caddy-vhosts/subcommands/label b/plugins/caddy-vhosts/subcommands/label deleted file mode 100755 index 2a5e0421b..000000000 --- a/plugins/caddy-vhosts/subcommands/label +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/caddy-vhosts/command-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-caddy-label "$@" diff --git a/plugins/haproxy-vhosts/command-functions b/plugins/haproxy-vhosts/command-functions index f7ce1be91..d7a4633c1 100755 --- a/plugins/haproxy-vhosts/command-functions +++ b/plugins/haproxy-vhosts/command-functions @@ -5,14 +5,28 @@ source "$PLUGIN_AVAILABLE_PATH/haproxy-vhosts/internal-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x -cmd-haproxy-label() { - declare desc="set/unset/show container proxy labels" - declare cmd="haproxy:label" +cmd-haproxy-label-add() { + declare desc="adds container label to app for specified proxy" + declare cmd="haproxy:label:add" [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - set -- haproxy "$@" - cmd-proxy-label "$@" + cmd-proxy-label-add "proxy:label:add" "haproxy" "$@" +} + +cmd-haproxy-label-remove() { + declare desc="removes container label from app for specified proxy" + declare cmd="haproxy:label:remove" + [[ "$1" == "$cmd" ]] && shift 1 + + cmd-proxy-label-remove "proxy:label:remove" "haproxy" "$@" +} + +cmd-haproxy-label-show() { + declare desc="shows container labels for app for specified proxy" + declare cmd="haproxy:label:show" + [[ "$1" == "$cmd" ]] && shift 1 + + cmd-proxy-label-show "proxy:label:show" "haproxy" "$@" } cmd-haproxy-report() { diff --git a/plugins/haproxy-vhosts/commands b/plugins/haproxy-vhosts/commands index 945869e42..fc866b7b8 100755 --- a/plugins/haproxy-vhosts/commands +++ b/plugins/haproxy-vhosts/commands @@ -1,6 +1,7 @@ #!/usr/bin/env bash [[ " haproxy:help help " == *" $1 "* ]] || exit "$DOKKU_NOT_IMPLEMENTED_EXIT" source "$PLUGIN_AVAILABLE_PATH/haproxy-vhosts/help-functions" +source "$PLUGIN_AVAILABLE_PATH/haproxy-vhosts/command-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x @@ -9,6 +10,18 @@ case "$1" in cmd-haproxy-help "$@" ;; + haproxy:label:add) + cmd-haproxy-label-add "$@" + ;; + + haproxy:label:remove) + cmd-haproxy-label-remove "$@" + ;; + + haproxy:label:show) + cmd-haproxy-label-show "$@" + ;; + *) exit "$DOKKU_NOT_IMPLEMENTED_EXIT" ;; diff --git a/plugins/haproxy-vhosts/subcommands/label b/plugins/haproxy-vhosts/subcommands/label deleted file mode 100755 index d52aa7b16..000000000 --- a/plugins/haproxy-vhosts/subcommands/label +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/haproxy-vhosts/command-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-haproxy-label "$@" diff --git a/plugins/openresty-vhosts/command-functions b/plugins/openresty-vhosts/command-functions index d9f6401c7..ef887cde4 100755 --- a/plugins/openresty-vhosts/command-functions +++ b/plugins/openresty-vhosts/command-functions @@ -5,14 +5,28 @@ source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/internal-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x -cmd-openresty-label() { - declare desc="set/unset/show container proxy labels" - declare cmd="openresty:label" +cmd-openresty-label-add() { + declare desc="adds container label to app for specified proxy" + declare cmd="openresty:label:add" [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - set -- openresty "$@" - cmd-proxy-label "$@" + cmd-proxy-label-add "proxy:label:add" "openresty" "$@" +} + +cmd-openresty-label-remove() { + declare desc="removes container label from app for specified proxy" + declare cmd="openresty:label:remove" + [[ "$1" == "$cmd" ]] && shift 1 + + cmd-proxy-label-remove "proxy:label:remove" "openresty" "$@" +} + +cmd-openresty-label-show() { + declare desc="shows container labels for app for specified proxy" + declare cmd="openresty:label:show" + [[ "$1" == "$cmd" ]] && shift 1 + + cmd-proxy-label-show "proxy:label:show" "openresty" "$@" } cmd-openresty-report() { diff --git a/plugins/openresty-vhosts/commands b/plugins/openresty-vhosts/commands index b87c8aa14..6b76d4e68 100755 --- a/plugins/openresty-vhosts/commands +++ b/plugins/openresty-vhosts/commands @@ -1,6 +1,7 @@ #!/usr/bin/env bash [[ " openresty:help help " == *" $1 "* ]] || exit "$DOKKU_NOT_IMPLEMENTED_EXIT" source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/help-functions" +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/command-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x @@ -9,6 +10,18 @@ case "$1" in cmd-openresty-help "$@" ;; + openresty:label:add) + cmd-openresty-label-add "$@" + ;; + + openresty:label:remove) + cmd-openresty-label-remove "$@" + ;; + + openresty:label:show) + cmd-openresty-label-show "$@" + ;; + *) exit "$DOKKU_NOT_IMPLEMENTED_EXIT" ;; diff --git a/plugins/openresty-vhosts/subcommands/label b/plugins/openresty-vhosts/subcommands/label deleted file mode 100755 index b1a49be45..000000000 --- a/plugins/openresty-vhosts/subcommands/label +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/command-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-openresty-label "$@" diff --git a/plugins/proxy/command-functions b/plugins/proxy/command-functions index d38158421..2bc6e1170 100755 --- a/plugins/proxy/command-functions +++ b/plugins/proxy/command-functions @@ -11,25 +11,27 @@ cmd-proxy-label-show() { declare PROXY="$1" APP="$2" PASSED_LABEL_NAME="$3" shift 2 + [[ -z "$PROXY" ]] && dokku_log_fail "Error invoking proxy:label command" + verify_app_name "$APP" + if [[ -n "$PASSED_LABEL_NAME" ]]; then fn-proxy-show-label "$PROXY" "$APP" "$PASSED_LABEL_NAME" return fi fn-proxy-display-labels "$PROXY" "$APP" - return 0 } cmd-proxy-label-add() { declare desc="adds container label to app for specified proxy" + declare cmd="proxy:label:add" + [[ "$1" == "$cmd" ]] && shift 1 declare PROXY="$1" APP="$2" PASSED_LABEL_NAME="$3" PASSED_LABEL_VALUE="$4" - if [[ -z "$PASSED_LABEL_NAME" ]]; then - dokku_log_fail "Please specify a container label for the app" - fi - if [[ -z "$PASSED_LABEL_VALUE" ]]; then - dokku_log_fail "Please specify a value for the container label" - fi + [[ -z "$PROXY" ]] && dokku_log_fail "Error invoking proxy:label command" + [[ -z "$PASSED_LABEL_NAME" ]] && dokku_log_fail "Please specify a container label for the app" + [[ -z "$PASSED_LABEL_VALUE" ]] && dokku_log_fail "Please specify a value for the container label" + verify_app_name "$APP" local proxy_labels_file_path="$(fn-proxy-get-labels-file-path "$PROXY" "$APP")" touch "$proxy_labels_file_path" @@ -41,11 +43,13 @@ cmd-proxy-label-add() { cmd-proxy-label-remove() { declare desc="removes container label from app for specified proxy" + declare cmd="proxy:label:remove" + [[ "$1" == "$cmd" ]] && shift 1 declare PROXY="$1" APP="$2" PASSED_LABEL_NAME="$3" - if [[ -z "$PASSED_LABEL_NAME" ]]; then - dokku_log_fail "Please specify a container label for the app" - fi + [[ -z "$PROXY" ]] && dokku_log_fail "Error invoking proxy:label command" + [[ -z "$PASSED_LABEL_NAME" ]] && dokku_log_fail "Please specify a container label for the app" + verify_app_name "$APP" local proxy_labels_file_path="$(fn-proxy-get-labels-file-path "$PROXY" "$APP")" if [[ ! -s "$proxy_labels_file_path" ]]; then @@ -57,28 +61,3 @@ cmd-proxy-label-remove() { local all_proxy_labels="$(echo -e "${all_proxy_labels}" | sed -E "s/^--label ['\"]${escaped_label_name}=.*\$//")" echo -e "${all_proxy_labels}" | sed '/^$/d' | sort -u >"$proxy_labels_file_path" } - -cmd-proxy-label() { - declare desc="manage proxy labels for an app" - declare cmd="proxy:label" - shift 1 # the command comes from the upstream plugin - declare PROXY="$1" ACTION="$2" APP="$3" PASSED_LABEL_NAME="$4" PASSED_LABEL_VALUE="$5" - - [[ -z "$PROXY" ]] && dokku_log_fail "Error invoking proxy:label command" - verify_app_name "$APP" - - if [[ -n "$PASSED_LABEL_NAME" ]] && [[ ! "$PASSED_LABEL_NAME" =~ ^$PROXY ]]; then - dokku_log_fail "Please specify a valid $PROXY label name for the app" - fi - - if [[ "$action" == "show" ]]; then - cmd-proxy-label-show "$PROXY" "$APP" "$PASSED_LABEL_NAME" - return 0 - elif [[ "$ACTION" == "set" ]]; then - cmd-proxy-label-add "$PROXY" "$APP" "$PASSED_LABEL_NAME" "$PASSED_LABEL_VALUE" - elif [[ "$action" == "unset" ]]; then - cmd-proxy-label-remove "$PROXY" "$APP" "$PASSED_LABEL_NAME" - else - dokku_log_fail "Please specify a valid action ('set', 'unset' or 'show')" - fi -} diff --git a/plugins/traefik-vhosts/command-functions b/plugins/traefik-vhosts/command-functions index 5a9eb91ec..32a3d9e75 100755 --- a/plugins/traefik-vhosts/command-functions +++ b/plugins/traefik-vhosts/command-functions @@ -5,14 +5,28 @@ source "$PLUGIN_AVAILABLE_PATH/traefik-vhosts/internal-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x -cmd-traefik-label() { - declare desc="set/unset/show container proxy labels" - declare cmd="traefik:label" +cmd-traefik-label-add() { + declare desc="adds container label to app for specified proxy" + declare cmd="traefik:label:add" [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - set -- traefik "$@" - cmd-proxy-label "$@" + cmd-proxy-label-add "proxy:label:add" "traefik" "$@" +} + +cmd-traefik-label-remove() { + declare desc="removes container label from app for specified proxy" + declare cmd="traefik:label:remove" + [[ "$1" == "$cmd" ]] && shift 1 + + cmd-proxy-label-remove "proxy:label:remove" "traefik" "$@" +} + +cmd-traefik-label-show() { + declare desc="shows container labels for app for specified proxy" + declare cmd="traefik:label:show" + [[ "$1" == "$cmd" ]] && shift 1 + + cmd-proxy-label-show "proxy:label:show" "traefik" "$@" } cmd-traefik-report() { diff --git a/plugins/traefik-vhosts/commands b/plugins/traefik-vhosts/commands index dfcc90d93..24a440dff 100755 --- a/plugins/traefik-vhosts/commands +++ b/plugins/traefik-vhosts/commands @@ -1,6 +1,7 @@ #!/usr/bin/env bash [[ " traefik:help help " == *" $1 "* ]] || exit "$DOKKU_NOT_IMPLEMENTED_EXIT" source "$PLUGIN_AVAILABLE_PATH/traefik-vhosts/help-functions" +source "$PLUGIN_AVAILABLE_PATH/traefik-vhosts/command-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x @@ -9,6 +10,18 @@ case "$1" in cmd-traefik-help "$@" ;; + traefik:label:add) + cmd-traefik-label-add "$@" + ;; + + traefik:label:remove) + cmd-traefik-label-remove "$@" + ;; + + traefik:label:show) + cmd-traefik-label-show "$@" + ;; + *) exit "$DOKKU_NOT_IMPLEMENTED_EXIT" ;; diff --git a/plugins/traefik-vhosts/subcommands/label b/plugins/traefik-vhosts/subcommands/label deleted file mode 100755 index 719f47b8b..000000000 --- a/plugins/traefik-vhosts/subcommands/label +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/traefik-vhosts/command-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-traefik-label "$@" From edf9653ebf7dacd78305b828a55913cfb405432c Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 16 Nov 2025 16:32:13 -0500 Subject: [PATCH 096/144] fix: update help output for new subcommands --- plugins/caddy-vhosts/help-functions | 4 +++- plugins/haproxy-vhosts/help-functions | 4 +++- plugins/openresty-vhosts/help-functions | 4 +++- plugins/traefik-vhosts/help-functions | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/plugins/caddy-vhosts/help-functions b/plugins/caddy-vhosts/help-functions index 76b161ef3..6ea1b1f3e 100755 --- a/plugins/caddy-vhosts/help-functions +++ b/plugins/caddy-vhosts/help-functions @@ -30,7 +30,9 @@ fn-help-content() { caddy:report [] [], Displays an caddy report for one or more apps caddy:set (), Set or clear an caddy property for an app caddy:show-config , Display caddy compose config - caddy:label (), Show, set or clear a caddy label for an app + caddy:label:add