This proposal outlines an approach to enable Go-based actions as an alternative technique for DDEV add-ons, while maintaining full support for existing bash-based actions. Both techniques will be equally supported, and add-on developers can choose the approach that best fits their needs. The solution uses Docker containers with Go runtime to build and execute Go source files on-demand, requiring only Docker (which DDEV already requires) on user machines.
DDEV add-ons currently use install.yaml with bash scripts:
name: my-addon
pre_install_actions:
- |
#ddev-description:Check something
if some_bash_condition; then
echo "Success"
else
exit 1
fi
post_install_actions:
- |
#ddev-description:Configure something
generate_config_filename: my-addon
# Option 1: Use bash actions (existing, fully supported)
pre_install_actions:
- |
#ddev-description:Validate environment
check_environment_bash_logic
post_install_actions:
- |
#ddev-description:Generate configs
generate_configs_bash_logic
# Option 2: Use Go actions (new alternative technique)
go_actions:
pre_install:
- action_name: "validate_environment"
go_source: "actions/validate.go"
- action_name: "check_dependencies"
go_source: "actions/check_deps.go"
post_install:
- action_name: "generate_configs"
go_source: "actions/generate.go"
# Option 3: Mix both approaches (both techniques work together)
pre_install_actions:
- |
#ddev-description:Quick bash check
simple_bash_validation
go_actions:
post_install:
- action_name: "complex_config_generation"
go_source: "actions/generate_complex_configs.go"// actions/validate.go
package main
func main() {
context := loadDdevContext()
if validateEnvironment(context) {
printSuccess("Environment validation passed")
} else {
printError("Environment validation failed")
exit(1)
}
}
func validateEnvironment(context DdevContext) bool {
// Type-safe validation logic
if context.hasService("database") {
return true
}
return false
}my-addon/
├── install.yaml
├── docker-compose.service.yaml
├── commands/host/my-command
└── actions/
├── go.mod
├── validate.go
├── check_deps.go
└── generate.go
When DDEV encounters a go_actions section, it:
- Mounts the add-on directory into a Go runtime container
- Provides DDEV context as JSON file to the Go program
- Builds and runs the Go source file using
go run - Returns the result just like bash actions
Pseudocode for execution:
func processGoAction(action GoAction, ddevContext map[string]interface{}) error {
contextFile := createTempContextFile(ddevContext)
dockerCommand := []string{
"docker", "run", "--rm",
"-v", addonDirectory + ":/addon:ro",
"-v", projectDirectory + ":/project",
"-v", contextFile + ":/context.json:ro",
"-e", "DDEV_ADDON_CONTEXT=/context.json",
"-w", "/addon",
"ddev/go-addon-runner:latest",
"go", "run", action.GoSource,
}
return executeDockerCommand(dockerCommand)
}The container includes:
- Go 1.23+ runtime
- Common dependencies (yaml parsing, HTTP clients, etc.)
- DDEV context utilities
- Standard Unix tools (jq, curl, bash)
All Go actions follow a standard pattern:
// Pseudocode interface
type AddonAction interface {
Execute(context DdevContext) error
Describe() string // For #ddev-description output
}
type DdevContext struct {
ProjectConfig map[string]interface{}
GlobalConfig map[string]interface{}
Environment map[string]string
ProjectRoot string
YamlFiles map[string]interface{}
}- Type Safety: Compile-time error checking vs runtime bash errors
- Better Error Handling: Structured error messages with context
- Testing: Unit testable Go code vs difficult-to-test bash
- IDE Support: Full IDE support for development and debugging
- Configuration Parsing: Robust YAML/JSON parsing with struct validation
- Data Transformation: Complex logic becomes maintainable
- Performance: Faster execution for heavy processing
- Modularity: Clean separation of concerns
// Pseudocode for Platform.sh-style complex transformation
type PlatformConfig struct {
App AppConfig
Services map[string]ServiceConfig
Routes map[string]RouteConfig
}
func transformPlatformToDdev(platform PlatformConfig) DdevConfig {
ddev := DdevConfig{}
// Type-safe transformations
ddev.PHPVersion = extractPHPVersion(platform.App.Type)
ddev.Database = transformDatabase(platform.Services)
ddev.AdditionalServices = transformServices(platform.Services)
ddev.Hooks = transformHooks(platform.App.Hooks)
return ddev
}- No Changes Required: Existing add-ons continue to work
- No Go Installation: Only Docker required (already needed for DDEV)
- Better Error Messages: More helpful error output from Go actions
- Faster Installation: Especially for complex add-ons
- Choice of Technique: Can choose bash, Go, or mix both based on needs
- Standard Tooling: Use familiar Go development tools when choosing Go
- Testing: Standard Go testing framework for Go actions
- Documentation: Generated from Go code comments for Go actions
- Create
ddev/go-addon-runnerDocker image - Extend
InstallDescstruct to supportgo_actions - Modify
ProcessAddonAction()to handle Go actions - Create DDEV context interface and utilities
- Add-on scaffolding tool (
ddev addon create --go) - Testing framework for Go actions
- Documentation and examples
- Best practices guide for choosing between bash and Go
- Create Go-based version of
ddev-phpmyadminas simple example - Create Go-based version of complex parts of
ddev-platformshas advanced example - Community documentation and best practices for both techniques
- Full Backward Compatibility: All existing bash-based add-ons continue to work unchanged
- Equal Support: Both bash and Go actions are first-class citizens with equal support
- Mix and Match: Add-ons can use bash actions, Go actions, or both together
- No Migration Required: Existing add-ons can stay bash-based indefinitely
- Developer Choice: Add-on developers choose the best technique for their use case
- No Breaking Changes: No impact on existing DDEV functionality
- Docker (already required by DDEV)
- No Go installation needed
- No additional system dependencies
- Go knowledge for writing Go actions
- Basic understanding of Docker (for testing)
- Standard Go development environment (for development only)
Here's how the phpMyAdmin add-on validation would look:
// actions/validate_db.go
package main
import (
"encoding/json"
"fmt"
"os"
)
type AddonContext struct {
DdevProjectConfig map[string]interface{} `json:"DdevProjectConfig"`
DdevGlobalConfig map[string]interface{} `json:"DdevGlobalConfig"`
}
func main() {
// Load context from file
contextFile := os.Getenv("DDEV_ADDON_CONTEXT")
if contextFile == "" {
fmt.Fprintf(os.Stderr, "DDEV_ADDON_CONTEXT not set\n")
os.Exit(1)
}
data, err := os.ReadFile(contextFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read context: %v\n", err)
os.Exit(1)
}
var ctx AddonContext
if err := json.Unmarshal(data, &ctx); err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse context: %v\n", err)
os.Exit(1)
}
// Validate database service exists
if err := validateDatabaseService(&ctx); err != nil {
fmt.Fprintf(os.Stderr, "Database validation failed: %v\n", err)
os.Exit(1)
}
fmt.Printf("#ddev-description:Check for db service\n")
}
func validateDatabaseService(ctx *AddonContext) error {
// Check global config for omitted containers
if globalConfig, ok := ctx.DdevGlobalConfig["omit_containers"]; ok {
if containers, ok := globalConfig.([]interface{}); ok {
for _, container := range containers {
if container == "db" {
return fmt.Errorf("unable to install the add-on because no db service was found")
}
}
}
}
// Check project config for omitted containers
if projectConfig, ok := ctx.DdevProjectConfig["omit_containers"]; ok {
if containers, ok := projectConfig.([]interface{}); ok {
for _, container := range containers {
if container == "db" {
return fmt.Errorf("unable to install the add-on because no db service was found")
}
}
}
}
return nil
}The key changes to DDEV core would be in pkg/ddevapp/addons.go:
// Enhanced InstallDesc struct
type InstallDesc struct {
Name string `yaml:"name"`
ProjectFiles []string `yaml:"project_files"`
GlobalFiles []string `yaml:"global_files,omitempty"`
DdevVersionConstraint string `yaml:"ddev_version_constraint,omitempty"`
Dependencies []string `yaml:"dependencies,omitempty"`
PreInstallActions []string `yaml:"pre_install_actions,omitempty"`
PostInstallActions []string `yaml:"post_install_actions,omitempty"`
RemovalActions []string `yaml:"removal_actions,omitempty"`
YamlReadFiles map[string]string `yaml:"yaml_read_files"`
// New Go actions support
GoActions *GoActionsConfig `yaml:"go_actions,omitempty"`
}
type GoActionsConfig struct {
PreInstall []GoAction `yaml:"pre_install,omitempty"`
PostInstall []GoAction `yaml:"post_install,omitempty"`
}
type GoAction struct {
Name string `yaml:"action_name"`
GoSource string `yaml:"go_source"`
Args []string `yaml:"args,omitempty"`
}
// Enhanced ProcessAddonAction function
func ProcessAddonAction(action interface{}, dict map[string]interface{}, bashPath string, verbose bool) error {
switch a := action.(type) {
case string:
// Legacy bash action (existing code)
return processBashAction(a, dict, bashPath, verbose)
case GoAction:
// New Go action
return processGoAction(a, dict, verbose)
default:
return fmt.Errorf("unsupported action type: %T", action)
}
}
func processGoAction(action GoAction, dict map[string]interface{}, verbose bool) error {
// Create temporary context file
contextFile := filepath.Join(os.TempDir(), "ddev-addon-context.json")
contextData, _ := json.Marshal(dict)
os.WriteFile(contextFile, contextData, 0644)
defer os.Remove(contextFile)
// Get current directories
addonDir := getCurrentAddonDir()
projectDir := getCurrentProjectRoot()
dockerCmd := []string{
"docker", "run", "--rm",
"-v", fmt.Sprintf("%s:/addon:ro", addonDir),
"-v", fmt.Sprintf("%s:/project", projectDir),
"-v", fmt.Sprintf("%s:/context.json:ro", contextFile),
"-e", "DDEV_ADDON_CONTEXT=/context.json",
"-w", "/addon",
"ddev/go-addon-runner:latest",
"go", "run", action.GoSource,
}
dockerCmd = append(dockerCmd, action.Args...)
if verbose {
fmt.Printf("Running Go action: %s\n", action.GoSource)
}
return exec.RunCommand("docker", dockerCmd...)
}This approach provides an alternative technique for DDEV add-on development, giving developers the choice between bash and Go while preserving the user-friendly, dependency-minimal nature of DDEV. Both bash and Go actions are equally supported and can coexist within the same add-on.
The key innovation is using Docker containers with Go runtime to build and execute Go source files on-demand, requiring only Docker (which DDEV already requires) on user machines. This enables more powerful, maintainable add-on logic for developers who prefer Go, while keeping bash as a fully supported option for those who prefer it.
Key Principles:
- Choice, not replacement: Go actions are an alternative, not a replacement for bash
- Equal support: Both techniques receive the same level of support and documentation
- Backward compatibility: All existing bash-based add-ons continue to work unchanged
- Flexibility: Add-ons can use bash, Go, or both techniques together