mirror of
https://github.com/dokku/dokku.git
synced 2026-02-23 19:50:34 +01:00
@@ -25,6 +25,103 @@
|
||||
- `schedule`: (string, required)
|
||||
- `concurrency_policy`: (string, optional, default: `allow`, options: `allow`, `forbid`, `replace`)
|
||||
|
||||
## Env
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"SIMPLE_VAR": "default_value",
|
||||
"SECRET_KEY": {
|
||||
"description": "A secret key for signing tokens",
|
||||
"generator": "secret"
|
||||
},
|
||||
"DATABASE_URL": {
|
||||
"description": "PostgreSQL connection string",
|
||||
"required": true
|
||||
},
|
||||
"OPTIONAL_VAR": {
|
||||
"description": "An optional configuration value",
|
||||
"value": "default",
|
||||
"required": false
|
||||
},
|
||||
"SYNC_VAR": {
|
||||
"description": "A variable that updates on every deploy",
|
||||
"value": "synced_value",
|
||||
"sync": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(object, optional) A key-value object for environment variable configuration. Keys are the variable names. Values can be either a string (used as the default value) or an object with the following properties:
|
||||
|
||||
- `description`: (string, optional) Human-readable explanation of the variable's purpose
|
||||
- `value`: (string, optional) Default value for the variable
|
||||
- `required`: (boolean, optional, default: `true`) Whether the variable must have a value
|
||||
- `generator`: (string, optional) Function to generate the value. Currently only `"secret"` is supported, which generates a 64-character cryptographically secure hex string
|
||||
- `sync`: (boolean, optional, default: `false`) If `true`, the value will be set on every deploy, overwriting any existing value
|
||||
|
||||
### Behavior
|
||||
|
||||
Environment variables from `app.json` are processed during the first deploy, before the predeploy script runs. The behavior depends on the variable configuration:
|
||||
|
||||
1. **Variables with `value` or simple string**: The default value is set if the variable doesn't already exist
|
||||
2. **Variables with `generator: "secret"`**: A random 64-character hex string is generated if the variable doesn't exist
|
||||
3. **Required variables without a value or generator**: If a TTY is available, the user is prompted for a value. Otherwise, the deploy fails with an error
|
||||
4. **Optional variables without a value**: Skipped silently if no TTY is available
|
||||
|
||||
On subsequent deploys:
|
||||
- Variables are NOT re-set unless `sync: true` is specified
|
||||
- Variables with `sync: true` are always set to their configured value, overwriting any manual changes
|
||||
- Variables that already have values are not modified
|
||||
|
||||
### Examples
|
||||
|
||||
**Simple default value:**
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"WEB_CONCURRENCY": "5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Generated secret (recommended for API keys, tokens, etc.):**
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"SECRET_KEY_BASE": {
|
||||
"description": "Base secret for session encryption",
|
||||
"generator": "secret"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required variable that must be provided:**
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"DATABASE_URL": {
|
||||
"description": "PostgreSQL connection URL",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Variable that stays in sync with app.json:**
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"FEATURE_FLAGS": {
|
||||
"value": "new_ui,dark_mode",
|
||||
"sync": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Formation
|
||||
|
||||
```json
|
||||
|
||||
@@ -71,6 +71,35 @@ dokku config:export --format shell node-js-app
|
||||
# ENV='prod' COMPILE_ASSETS='1'
|
||||
```
|
||||
|
||||
## Setting Environment Variables via app.json
|
||||
|
||||
Environment variables can also be declared in an `app.json` file in your repository root. This is useful for setting default values, generating secrets, or requiring certain variables to be set before deployment.
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"SIMPLE_VAR": "default_value",
|
||||
"SECRET_KEY": {
|
||||
"description": "A secret key for signing tokens",
|
||||
"generator": "secret"
|
||||
},
|
||||
"DATABASE_URL": {
|
||||
"description": "PostgreSQL connection string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Environment variables from `app.json` are processed during the first deploy, before the predeploy script runs. Variables can be configured as:
|
||||
|
||||
- **Simple string values**: Set as defaults if not already configured
|
||||
- **Generated secrets**: Use `"generator": "secret"` to auto-generate a 64-character hex string
|
||||
- **Required variables**: Use `"required": true` to prompt for or require a value
|
||||
- **Synced variables**: Use `"sync": true` to update the value on every deploy
|
||||
|
||||
For full details on the `env` schema and behavior, see the [app.json documentation](/docs/appendices/file-formats/app-json.md#env).
|
||||
|
||||
## Special Config Variables
|
||||
|
||||
The following config variables have special meanings and can be set in a variety of ways. Unless specified via global app config, the values may not be passed into applications. Usage of these values within applications should be considered unsafe, as they are an internal configuration values that may be moved to the internal properties system in the future.
|
||||
|
||||
@@ -521,6 +521,21 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
# TODO
|
||||
```
|
||||
|
||||
### `config-set`
|
||||
|
||||
- Description: Sets one or more config values for an app without restarting (when --no-restart flag is specified)
|
||||
- Invoked by: app-json plugin
|
||||
- Arguments: `[--no-restart] $APP $KEY1=VALUE1 [$KEY2=VALUE2 ...]`
|
||||
- Example:
|
||||
|
||||
```shell
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
|
||||
# TODO
|
||||
```
|
||||
|
||||
### `core-post-deploy`
|
||||
|
||||
> To avoid issues with community plugins, this plugin trigger should be used _only_ for core plugins. Please avoid using this trigger in your own plugins.
|
||||
|
||||
@@ -24,11 +24,53 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// EnvVarValue represents an environment variable definition in app.json
|
||||
// It supports both simple string values and complex object definitions
|
||||
type EnvVarValue struct {
|
||||
// Description provides context for the env var (used in prompts)
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
// Value is the default value for the env var
|
||||
Value string `json:"value,omitempty"`
|
||||
|
||||
// Required indicates if the env var must have a value (defaults to true per Heroku spec)
|
||||
Required *bool `json:"required,omitempty"`
|
||||
|
||||
// Generator specifies how to auto-generate a value (currently only "secret" is supported)
|
||||
Generator string `json:"generator,omitempty"`
|
||||
|
||||
// Sync indicates if the value should be set on every deploy, not just first
|
||||
Sync bool `json:"sync,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON handles both string and object formats for env vars
|
||||
func (e *EnvVarValue) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err == nil {
|
||||
e.Value = str
|
||||
return nil
|
||||
}
|
||||
|
||||
type envVarAlias EnvVarValue
|
||||
return json.Unmarshal(data, (*envVarAlias)(e))
|
||||
}
|
||||
|
||||
// IsRequired returns true if the env var is required (defaults to true per Heroku spec)
|
||||
func (e *EnvVarValue) IsRequired() bool {
|
||||
if e.Required == nil {
|
||||
return true
|
||||
}
|
||||
return *e.Required
|
||||
}
|
||||
|
||||
// AppJSON is a struct that represents an app.json file as understood by Dokku
|
||||
type AppJSON struct {
|
||||
// Cron is a list of cron tasks to execute
|
||||
Cron []CronTask `json:"cron"`
|
||||
|
||||
// Env is a map of environment variables to set
|
||||
Env map[string]EnvVarValue `json:"env,omitempty"`
|
||||
|
||||
// Formation is a map of process types to scale
|
||||
Formation map[string]Formation `json:"formation"`
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
package appjson
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
func constructScript(command string, shell string, isHerokuishImage bool, isCnbImage bool, dockerfileEntrypoint string) []string {
|
||||
@@ -455,3 +460,199 @@ func setScale(appName string) error {
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// generateSecret generates a cryptographically secure random hex string of specified length
|
||||
func generateSecret(length int) (string, error) {
|
||||
bytes := make([]byte, length/2)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", fmt.Errorf("failed to generate secret: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// isInteractive returns true if stdin is a terminal (TTY available)
|
||||
func isInteractive() bool {
|
||||
return isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())
|
||||
}
|
||||
|
||||
// promptForValue prompts the user for an environment variable value
|
||||
func promptForValue(varName string, envVar EnvVarValue) (string, error) {
|
||||
prompt := fmt.Sprintf("Enter value for %s", varName)
|
||||
if envVar.Description != "" {
|
||||
prompt = fmt.Sprintf("Enter value for %s (%s)", varName, envVar.Description)
|
||||
}
|
||||
|
||||
common.LogInfo1(prompt)
|
||||
fmt.Print("> ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.TrimSpace(response), nil
|
||||
}
|
||||
|
||||
// getCurrentConfig retrieves the current config values for an app
|
||||
func getCurrentConfig(appName string) (map[string]string, error) {
|
||||
results, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
||||
Trigger: "config-export",
|
||||
Args: []string{appName, "false", "true", "json"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current config: %w", err)
|
||||
}
|
||||
|
||||
var currentConfig map[string]string
|
||||
if err := json.Unmarshal(results.StdoutBytes(), ¤tConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse current config: %w", err)
|
||||
}
|
||||
|
||||
return currentConfig, nil
|
||||
}
|
||||
|
||||
// setConfigVars sets multiple config vars for an app without triggering restart
|
||||
func setConfigVars(appName string, vars map[string]string) error {
|
||||
if len(vars) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build arguments for config-set trigger: --no-restart flag, appName, key=value pairs
|
||||
args := []string{"--no-restart", appName}
|
||||
for key, value := range vars {
|
||||
args = append(args, fmt.Sprintf("%s=%s", key, value))
|
||||
}
|
||||
|
||||
_, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
|
||||
Trigger: "config-set",
|
||||
Args: args,
|
||||
StreamStdio: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set config vars: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processAppJSONEnv processes env vars from app.json and sets them as config
|
||||
func processAppJSONEnv(appName string) error {
|
||||
appJSON, err := GetAppJSON(appName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(appJSON.Env) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
common.LogInfo1("Processing app.json env vars")
|
||||
|
||||
isFirstDeploy := common.PropertyGet("common", appName, "deployed") != "true"
|
||||
|
||||
// Check if env vars have been processed for this deploy
|
||||
envProcessedProperty := "appjson-env-processed"
|
||||
alreadyProcessed := common.PropertyGet("app-json", appName, envProcessedProperty) == "executed"
|
||||
|
||||
// Get current config values
|
||||
currentConfig, err := getCurrentConfig(appName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
varsToSet := make(map[string]string)
|
||||
interactive := isInteractive()
|
||||
|
||||
// Sort keys for deterministic ordering
|
||||
var keys []string
|
||||
for k := range appJSON.Env {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, varName := range keys {
|
||||
envVar := appJSON.Env[varName]
|
||||
|
||||
// Determine if we should process this var
|
||||
shouldProcess := false
|
||||
if isFirstDeploy && !alreadyProcessed {
|
||||
// First deploy: process all vars
|
||||
shouldProcess = true
|
||||
} else if envVar.Sync {
|
||||
// Subsequent deploy with sync: always process sync vars
|
||||
shouldProcess = true
|
||||
}
|
||||
|
||||
if !shouldProcess {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if already set (for first deploy, skip if already has value and not sync)
|
||||
currentValue, hasValue := currentConfig[varName]
|
||||
if hasValue && currentValue != "" && !envVar.Sync {
|
||||
common.LogDebug(fmt.Sprintf("Skipping %s: already set", varName))
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine the value
|
||||
var value string
|
||||
|
||||
// Handle generator
|
||||
if envVar.Generator == "secret" {
|
||||
if !hasValue || currentValue == "" || envVar.Sync {
|
||||
generated, err := generateSecret(64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate secret for %s: %w", varName, err)
|
||||
}
|
||||
value = generated
|
||||
common.LogDebug(fmt.Sprintf("Generated secret for %s", varName))
|
||||
} else {
|
||||
continue // Already has value, not sync
|
||||
}
|
||||
} else if envVar.Value != "" {
|
||||
// Use default value
|
||||
value = envVar.Value
|
||||
} else if interactive {
|
||||
// Prompt user
|
||||
prompted, err := promptForValue(varName, envVar)
|
||||
if err != nil {
|
||||
if envVar.IsRequired() {
|
||||
return fmt.Errorf("required env var %s has no value and prompt failed: %w", varName, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if prompted == "" && envVar.IsRequired() {
|
||||
return fmt.Errorf("required env var %s cannot be empty", varName)
|
||||
}
|
||||
value = prompted
|
||||
} else {
|
||||
// No TTY, no default, no generator
|
||||
if envVar.IsRequired() {
|
||||
return fmt.Errorf("required env var %s has no value, no default, and no TTY for prompt", varName)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if value != "" {
|
||||
varsToSet[varName] = value
|
||||
}
|
||||
}
|
||||
|
||||
if len(varsToSet) == 0 {
|
||||
common.LogVerbose("No env vars to set from app.json")
|
||||
if !isFirstDeploy {
|
||||
return common.PropertyWrite("app-json", appName, envProcessedProperty, "executed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set the config vars without restart (we're in middle of deploy)
|
||||
common.LogInfo1(fmt.Sprintf("Setting %d env var(s) from app.json", len(varsToSet)))
|
||||
if err := setConfigVars(appName, varsToSet); err != nil {
|
||||
return fmt.Errorf("failed to set env vars: %w", err)
|
||||
}
|
||||
|
||||
// Mark as processed
|
||||
return common.PropertyWrite("app-json", appName, envProcessedProperty, "executed")
|
||||
}
|
||||
|
||||
@@ -71,6 +71,11 @@ func TriggerCorePostDeploy(appName string) error {
|
||||
|
||||
// TriggerCorePostExtract ensures that the main app.json is the one specified by app-json-path
|
||||
func TriggerCorePostExtract(appName string, sourceWorkDir string) error {
|
||||
// Clear the env-processed property on new source extraction to allow re-processing
|
||||
if err := common.PropertyDelete("app-json", appName, "appjson-env-processed"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
destination := common.GetAppDataDirectory("app-json", appName)
|
||||
appJSONPath := strings.Trim(reportComputedAppjsonpath(appName), "/")
|
||||
if appJSONPath == "" {
|
||||
@@ -190,7 +195,13 @@ func TriggerPostDeploy(appName string, imageTag string) error {
|
||||
return executeScript(appName, image, imageTag, "postdeploy")
|
||||
}
|
||||
|
||||
// TriggerPreReleaseBuilder processes app.json env vars and executes the predeploy task
|
||||
func TriggerPreReleaseBuilder(builderType string, appName string, image string) error {
|
||||
// Process app.json env vars before predeploy script
|
||||
if err := processAppJSONEnv(appName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parts := strings.Split(image, ":")
|
||||
imageTag := parts[len(parts)-1]
|
||||
return executeScript(appName, image, imageTag, "predeploy")
|
||||
|
||||
@@ -74,7 +74,7 @@ trigger-builder-herokuish-builder-build() {
|
||||
fn-builder-herokuish-ensure-cache "$APP"
|
||||
if ! CID=$("$DOCKER_BIN" container create "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS -v "cache-$APP:/cache" --env=CACHE_PATH=/cache "${ARG_ARRAY[@]}" "$IMAGE" /build); then
|
||||
plugn trigger scheduler-register-retired "$APP" "$CID"
|
||||
dokku_log_warn "Failure during app build"
|
||||
dokku_log_warn "Failure creating container during app build"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -82,14 +82,14 @@ trigger-builder-herokuish-builder-build() {
|
||||
"$DOCKER_BIN" container start "$CID" >/dev/null || DOKKU_CONTAINER_EXIT_CODE=$?
|
||||
if ! "$DOCKER_BIN" container attach "$CID"; then
|
||||
plugn trigger scheduler-register-retired "$APP" "$CID"
|
||||
dokku_log_warn "Failure during app build"
|
||||
dokku_log_warn "Failure starting container during app build"
|
||||
return 1
|
||||
fi
|
||||
|
||||
DOKKU_CONTAINER_EXIT_CODE="$("$DOCKER_BIN" container wait "$CID" 2>/dev/null || echo "$DOKKU_CONTAINER_EXIT_CODE")"
|
||||
if [[ "$DOKKU_CONTAINER_EXIT_CODE" -ne 0 ]]; then
|
||||
plugn trigger scheduler-register-retired "$APP" "$CID"
|
||||
dokku_log_warn "Failure during app build"
|
||||
dokku_log_warn "Failure attaching to container during app build"
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
GOARCH ?= amd64
|
||||
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
|
||||
TRIGGERS = triggers/config-export triggers/config-get triggers/config-get-global triggers/config-set triggers/config-unset triggers/post-app-clone-setup triggers/post-app-rename-setup
|
||||
BUILD = commands config_sub subcommands triggers
|
||||
PLUGIN_NAME = config
|
||||
|
||||
|
||||
@@ -65,3 +65,14 @@ config_bundle() {
|
||||
declare desc="export tar bundle of config"
|
||||
config_sub bundle "$@"
|
||||
}
|
||||
|
||||
fn-config-set() {
|
||||
declare desc="set config values via trigger (supports --no-restart)"
|
||||
declare APP="$1" NO_RESTART="$2"
|
||||
shift 2
|
||||
if [[ "$NO_RESTART" == "true" ]]; then
|
||||
plugn trigger config-set --no-restart "$APP" "$@"
|
||||
else
|
||||
plugn trigger config-set "$APP" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ func main() {
|
||||
parts := strings.Split(os.Args[0], "/")
|
||||
trigger := parts[len(parts)-1]
|
||||
global := flag.Bool("global", false, "--global: Whether global or app-specific")
|
||||
noRestart := flag.Bool("no-restart", false, "--no-restart: Whether to skip restart")
|
||||
flag.Parse()
|
||||
|
||||
var err error
|
||||
@@ -36,6 +37,10 @@ func main() {
|
||||
case "config-get-global":
|
||||
key := flag.Arg(0)
|
||||
err = config.TriggerConfigGetGlobal(key)
|
||||
case "config-set":
|
||||
appName := flag.Arg(0)
|
||||
pairs := flag.Args()[1:]
|
||||
err = config.TriggerConfigSet(appName, *noRestart, pairs...)
|
||||
case "config-unset":
|
||||
appName := flag.Arg(0)
|
||||
key := flag.Arg(1)
|
||||
|
||||
@@ -45,6 +45,11 @@ func TriggerConfigGetGlobal(key string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriggerConfigSet sets config values for an app
|
||||
func TriggerConfigSet(appName string, noRestart bool, pairs ...string) error {
|
||||
return SubSet(appName, pairs, noRestart, false)
|
||||
}
|
||||
|
||||
// TriggerConfigUnset unsets an app config value by key
|
||||
func TriggerConfigUnset(appName string, key string, restart bool) error {
|
||||
UnsetMany(appName, []string{key}, restart)
|
||||
|
||||
8
tests/apps/python/app-env-required.json
Normal file
8
tests/apps/python/app-env-required.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"env": {
|
||||
"REQUIRED_VAR": {
|
||||
"description": "A required variable with no default",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
27
tests/apps/python/app-env.json
Normal file
27
tests/apps/python/app-env.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"env": {
|
||||
"SIMPLE_VAR": "simple_value",
|
||||
"OBJECT_VAR": {
|
||||
"description": "A variable with a default value",
|
||||
"value": "object_default"
|
||||
},
|
||||
"SECRET_VAR": {
|
||||
"description": "A generated secret",
|
||||
"generator": "secret"
|
||||
},
|
||||
"OPTIONAL_VAR": {
|
||||
"description": "An optional variable",
|
||||
"required": false
|
||||
},
|
||||
"SYNC_VAR": {
|
||||
"description": "A variable that syncs on every deploy",
|
||||
"value": "sync_value",
|
||||
"sync": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dokku": {
|
||||
"predeploy": "echo ENV_CHECK: SIMPLE_VAR=$SIMPLE_VAR OBJECT_VAR=$OBJECT_VAR"
|
||||
}
|
||||
}
|
||||
}
|
||||
198
tests/unit/app-json-5.bats
Normal file
198
tests/unit/app-json-5.bats
Normal file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env bats
|
||||
load test_helper
|
||||
|
||||
setup() {
|
||||
global_setup
|
||||
create_app
|
||||
}
|
||||
|
||||
teardown() {
|
||||
destroy_app
|
||||
global_teardown
|
||||
}
|
||||
|
||||
@test "(app-json) app.json env simple value" {
|
||||
run /bin/bash -c "dokku app-json:set $TEST_APP appjson-path app-env.json"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output_contains "Processing app.json env vars"
|
||||
assert_output_contains "Setting 4 env var(s) from app.json"
|
||||
|
||||
run /bin/bash -c "dokku config:get $TEST_APP SIMPLE_VAR"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "simple_value"
|
||||
|
||||
run /bin/bash -c "dokku config:get $TEST_APP OBJECT_VAR"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "object_default"
|
||||
}
|
||||
|
||||
@test "(app-json) app.json env secret generator" {
|
||||
run /bin/bash -c "dokku app-json:set $TEST_APP appjson-path app-env.json"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku config:get $TEST_APP SECRET_VAR"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
# Secret should be 64 characters (hex encoded 32 bytes)
|
||||
[[ ${#output} -eq 64 ]]
|
||||
}
|
||||
|
||||
@test "(app-json) app.json env does not overwrite on redeploy" {
|
||||
run /bin/bash -c "dokku app-json:set $TEST_APP appjson-path app-env.json"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
# Get the original secret
|
||||
run /bin/bash -c "dokku config:get $TEST_APP SECRET_VAR"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
local original_secret="$output"
|
||||
|
||||
# Change SIMPLE_VAR manually
|
||||
run /bin/bash -c "dokku config:set --no-restart $TEST_APP SIMPLE_VAR=changed_value"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
# Rebuild the app
|
||||
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
# Verify SIMPLE_VAR was NOT overwritten
|
||||
run /bin/bash -c "dokku config:get $TEST_APP SIMPLE_VAR"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "changed_value"
|
||||
|
||||
# Verify SECRET_VAR was NOT regenerated
|
||||
run /bin/bash -c "dokku config:get $TEST_APP SECRET_VAR"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "$original_secret"
|
||||
}
|
||||
|
||||
@test "(app-json) app.json env sync overwrites on redeploy" {
|
||||
run /bin/bash -c "dokku app-json:set $TEST_APP appjson-path app-env.json"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
# Verify SYNC_VAR is set
|
||||
run /bin/bash -c "dokku config:get $TEST_APP SYNC_VAR"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "sync_value"
|
||||
|
||||
# Change SYNC_VAR manually
|
||||
run /bin/bash -c "dokku config:set --no-restart $TEST_APP SYNC_VAR=manual_value"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
# Rebuild the app
|
||||
run /bin/bash -c "dokku ps:rebuild $TEST_APP"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
# Verify SYNC_VAR WAS overwritten back to sync_value
|
||||
run /bin/bash -c "dokku config:get $TEST_APP SYNC_VAR"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "sync_value"
|
||||
}
|
||||
|
||||
@test "(app-json) app.json env required without value fails" {
|
||||
run /bin/bash -c "dokku app-json:set $TEST_APP appjson-path app-env-required.json"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "required env var REQUIRED_VAR has no value"
|
||||
}
|
||||
|
||||
@test "(app-json) app.json env skips optional without value" {
|
||||
run /bin/bash -c "dokku app-json:set $TEST_APP appjson-path app-env.json"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
# OPTIONAL_VAR should not be set since it has no default and is optional
|
||||
# config:get returns exit code 1 when a variable is not set
|
||||
run /bin/bash -c "dokku config:get $TEST_APP OPTIONAL_VAR"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output ""
|
||||
}
|
||||
|
||||
@test "(app-json) app.json env respects pre-set values on first deploy" {
|
||||
run /bin/bash -c "dokku app-json:set $TEST_APP appjson-path app-env.json"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
# Pre-set SIMPLE_VAR before first deploy
|
||||
run /bin/bash -c "dokku config:set --no-restart $TEST_APP SIMPLE_VAR=preset_value"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
# Verify SIMPLE_VAR was NOT overwritten
|
||||
run /bin/bash -c "dokku config:get $TEST_APP SIMPLE_VAR"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "preset_value"
|
||||
}
|
||||
Reference in New Issue
Block a user