feat: allow replacing buildpack list atomically
Add buildpacks:set --replace so callers can replace an app's complete ordered buildpack list in one command while preserving existing single-buildpack and --index behavior. Closes #8802
This commit is contained in:
@@ -10,6 +10,7 @@ buildpacks:list <app> # List all buildpacks fo
|
||||
buildpacks:remove <app> <buildpack> # Remove a buildpack set on the app
|
||||
buildpacks:report [<app>] [<flag>] # Displays a buildpack report for one or more apps
|
||||
buildpacks:set [--index 1] <app> <buildpack> # Set new app buildpack at a given position defaulting to the first buildpack if no index is specified
|
||||
buildpacks:set --replace <app> <buildpack> [<buildpack> ...] # Replace the entire ordered buildpack list with the specified buildpacks
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -86,6 +87,25 @@ If the index specified is larger than the number of buildpacks currently configu
|
||||
dokku buildpacks:set --index 99 node-js-app https://github.com/heroku/heroku-buildpack-ruby.git
|
||||
```
|
||||
|
||||
### Replacing the entire buildpack list
|
||||
|
||||
To replace the complete ordered buildpack list in a single command, use the `--replace` flag. This is useful for tooling that needs to apply a full buildpack list atomically instead of running `buildpacks:clear` followed by multiple `buildpacks:add` calls.
|
||||
|
||||
```shell
|
||||
dokku buildpacks:set --replace node-js-app https://github.com/heroku/heroku-buildpack-ruby.git https://github.com/heroku/heroku-buildpack-nodejs.git
|
||||
```
|
||||
|
||||
The buildpacks are executed in the order they are specified, and the previous list is discarded. If any specified buildpack is invalid, the existing list is left unchanged.
|
||||
|
||||
A single buildpack may also be specified to replace the entire list with just that buildpack:
|
||||
|
||||
```shell
|
||||
dokku buildpacks:set --replace node-js-app https://github.com/heroku/heroku-buildpack-ruby.git
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The `--replace` flag cannot be combined with the `--index` flag. To remove all buildpacks, use the `buildpacks:clear` command instead of `--replace` with no buildpacks.
|
||||
|
||||
### Removing a buildpack
|
||||
|
||||
> At least one of a buildpack or index must be specified
|
||||
|
||||
@@ -139,3 +139,79 @@ func TestGetBuildpacksAppJSONEmptyArray(t *testing.T) {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(buildpacks).To(BeEmpty())
|
||||
}
|
||||
|
||||
func TestSetBuildpackListReplacesEntireList(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
tmpDir := setupTestEnvironment(t)
|
||||
defer teardownTestEnvironment(tmpDir)
|
||||
|
||||
propDir := filepath.Join(tmpDir, "config", "buildpacks", "test-app")
|
||||
Expect(os.MkdirAll(propDir, 0755)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(propDir, "buildpacks"), []byte("https://github.com/heroku/heroku-buildpack-nodejs.git\nhttps://github.com/heroku/heroku-buildpack-python.git\nhttps://github.com/heroku/heroku-buildpack-ruby.git\n"), 0644)).To(Succeed())
|
||||
|
||||
err := setBuildpackList("test-app", []string{"heroku/ruby", "heroku/nodejs"}, 0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
buildpacks, err := getBuildpacks("test-app")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(buildpacks).To(HaveLen(2))
|
||||
Expect(buildpacks[0]).To(Equal("https://github.com/heroku/heroku-buildpack-ruby.git"))
|
||||
Expect(buildpacks[1]).To(Equal("https://github.com/heroku/heroku-buildpack-nodejs.git"))
|
||||
}
|
||||
|
||||
func TestSetBuildpackListSingleBuildpack(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
tmpDir := setupTestEnvironment(t)
|
||||
defer teardownTestEnvironment(tmpDir)
|
||||
|
||||
propDir := filepath.Join(tmpDir, "config", "buildpacks", "test-app")
|
||||
Expect(os.MkdirAll(propDir, 0755)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(propDir, "buildpacks"), []byte("https://github.com/heroku/heroku-buildpack-nodejs.git\nhttps://github.com/heroku/heroku-buildpack-python.git\n"), 0644)).To(Succeed())
|
||||
|
||||
err := setBuildpackList("test-app", []string{"heroku/ruby"}, 0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
buildpacks, err := getBuildpacks("test-app")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(buildpacks).To(HaveLen(1))
|
||||
Expect(buildpacks[0]).To(Equal("https://github.com/heroku/heroku-buildpack-ruby.git"))
|
||||
}
|
||||
|
||||
func TestSetBuildpackListInvalidLeavesListUnchanged(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
tmpDir := setupTestEnvironment(t)
|
||||
defer teardownTestEnvironment(tmpDir)
|
||||
|
||||
propDir := filepath.Join(tmpDir, "config", "buildpacks", "test-app")
|
||||
Expect(os.MkdirAll(propDir, 0755)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(propDir, "buildpacks"), []byte("https://github.com/heroku/heroku-buildpack-nodejs.git\n"), 0644)).To(Succeed())
|
||||
|
||||
err := setBuildpackList("test-app", []string{"heroku/ruby", "nodejs"}, 0)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("Invalid buildpack specified"))
|
||||
|
||||
buildpacks, err := getBuildpacks("test-app")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(buildpacks).To(HaveLen(1))
|
||||
Expect(buildpacks[0]).To(Equal("https://github.com/heroku/heroku-buildpack-nodejs.git"))
|
||||
}
|
||||
|
||||
func TestSetBuildpackListRejectsIndex(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
tmpDir := setupTestEnvironment(t)
|
||||
defer teardownTestEnvironment(tmpDir)
|
||||
|
||||
err := setBuildpackList("test-app", []string{"heroku/ruby"}, 1)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("--index"))
|
||||
}
|
||||
|
||||
func TestSetBuildpackListRejectsEmpty(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
tmpDir := setupTestEnvironment(t)
|
||||
defer teardownTestEnvironment(tmpDir)
|
||||
|
||||
err := setBuildpackList("test-app", []string{}, 0)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("buildpacks:clear"))
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ Additional commands:`
|
||||
buildpacks:remove <app> <buildpack>, Remove a buildpack set on the app
|
||||
buildpacks:report [<app>] [<flag>], Displays a buildpack report for one or more apps
|
||||
buildpacks:set [--index 1] <app> <buildpack>, Set new app buildpack at a given position defaulting to the first buildpack if no index is specified
|
||||
buildpacks:set --replace <app> <buildpack> [<buildpack> ...], Replace the entire ordered buildpack list with the specified buildpacks
|
||||
buildpacks:set-property [--global|<app>] <key> <value>, Set or clear a buildpacks property for an app`
|
||||
)
|
||||
|
||||
|
||||
@@ -66,10 +66,11 @@ func main() {
|
||||
case "set":
|
||||
args := flag.NewFlagSet("buildpacks:set", flag.ExitOnError)
|
||||
index := args.Int("index", 0, "--index: the 1-based index of the URL in the list of URLs")
|
||||
replace := args.Bool("replace", false, "--replace: replace the entire ordered buildpack list")
|
||||
args.Parse(os.Args[2:])
|
||||
appName := args.Arg(0)
|
||||
buildpack := args.Arg(1)
|
||||
err = buildpacks.CommandSet(appName, buildpack, *index)
|
||||
_, buildpackList := common.ShiftString(args.Args())
|
||||
err = buildpacks.CommandSet(appName, buildpackList, *index, *replace)
|
||||
case "set-property":
|
||||
args := flag.NewFlagSet("buildpacks:set-property", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: set a global property")
|
||||
|
||||
@@ -162,12 +162,24 @@ func CommandReport(appName string, format string, infoFlag string) error {
|
||||
}
|
||||
|
||||
// CommandSet implements buildpacks:set
|
||||
func CommandSet(appName string, buildpack string, index int) error {
|
||||
func CommandSet(appName string, buildpacks []string, index int, replace bool) error {
|
||||
if err := common.VerifyAppName(appName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buildpack, err := validBuildpackURL(buildpack)
|
||||
if replace {
|
||||
return setBuildpackList(appName, buildpacks, index)
|
||||
}
|
||||
|
||||
if len(buildpacks) > 1 {
|
||||
return errors.New("Setting multiple buildpacks requires the --replace flag")
|
||||
}
|
||||
|
||||
if len(buildpacks) == 0 {
|
||||
return errors.New("Must specify a buildpack")
|
||||
}
|
||||
|
||||
buildpack, err := validBuildpackURL(buildpacks[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -179,6 +191,28 @@ func CommandSet(appName string, buildpack string, index int) error {
|
||||
return common.PropertyListSet("buildpacks", appName, "buildpacks", buildpack, index)
|
||||
}
|
||||
|
||||
// setBuildpackList atomically replaces the entire ordered buildpack list
|
||||
func setBuildpackList(appName string, buildpacks []string, index int) error {
|
||||
if index != 0 {
|
||||
return errors.New("The --index flag cannot be used with --replace")
|
||||
}
|
||||
|
||||
if len(buildpacks) == 0 {
|
||||
return errors.New("Must specify at least one buildpack, use buildpacks:clear to remove all buildpacks")
|
||||
}
|
||||
|
||||
validated := make([]string, 0, len(buildpacks))
|
||||
for _, buildpack := range buildpacks {
|
||||
url, err := validBuildpackURL(buildpack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
validated = append(validated, url)
|
||||
}
|
||||
|
||||
return common.PropertyListWrite("buildpacks", appName, "buildpacks", validated)
|
||||
}
|
||||
|
||||
// CommandSetProperty implements buildpacks:set-property
|
||||
func CommandSetProperty(appName string, property string, value string) error {
|
||||
oldStack := ""
|
||||
|
||||
@@ -184,6 +184,75 @@ teardown() {
|
||||
assert_output "https://github.com/heroku/heroku-buildpack-golang.git https://github.com/heroku/heroku-buildpack-python.git https://github.com/heroku/heroku-buildpack-php.git"
|
||||
}
|
||||
|
||||
@test "(buildpacks) buildpacks:set --replace - failure" {
|
||||
run /bin/bash -c "dokku buildpacks:set --replace $TEST_APP"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "buildpacks:clear"
|
||||
|
||||
run /bin/bash -c "dokku buildpacks:set --replace --index 1 $TEST_APP heroku/nodejs"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "--index"
|
||||
|
||||
run /bin/bash -c "dokku buildpacks:set $TEST_APP heroku/nodejs heroku/ruby"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "--replace"
|
||||
}
|
||||
|
||||
@test "(buildpacks) buildpacks:set --replace - atomicity" {
|
||||
run /bin/bash -c "dokku buildpacks:add $TEST_APP heroku/nodejs"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku buildpacks:set --replace $TEST_APP heroku/ruby nodejs"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
|
||||
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_output "https://github.com/heroku/heroku-buildpack-nodejs.git"
|
||||
}
|
||||
|
||||
@test "(buildpacks) buildpacks:set --replace - success" {
|
||||
run /bin/bash -c "dokku buildpacks:add $TEST_APP heroku/nodejs"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku buildpacks:add $TEST_APP heroku/python"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku buildpacks:set --replace $TEST_APP heroku/ruby heroku/golang heroku/php"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_output "https://github.com/heroku/heroku-buildpack-ruby.git https://github.com/heroku/heroku-buildpack-golang.git https://github.com/heroku/heroku-buildpack-php.git"
|
||||
|
||||
run /bin/bash -c "dokku buildpacks:set --replace $TEST_APP heroku/nodejs"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku --quiet buildpacks:list $TEST_APP | xargs"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_output "https://github.com/heroku/heroku-buildpack-nodejs.git"
|
||||
}
|
||||
|
||||
@test "(buildpacks) buildpacks:set-property" {
|
||||
run /bin/bash -c "dokku buildpacks:set-property --global stack gliderlabs/herokuish:latest"
|
||||
echo "output: $output"
|
||||
|
||||
Reference in New Issue
Block a user