diff --git a/process-compose.override.yaml b/process-compose.override.yaml index aa4da7f..2624e2a 100644 --- a/process-compose.override.yaml +++ b/process-compose.override.yaml @@ -17,6 +17,10 @@ vars: PWD: /tmp DISABLED: true PORT: 80 +env_cmds: + DATE: "date" + OS_NAME: "awk -F= '/PRETTY/ {print $2}' /etc/os-release" + UPTIME: "uptime -p" processes: process0: @@ -124,6 +128,9 @@ processes: success_threshold: 1 failure_threshold: 3 + env-cmd-test: + command: "echo Date: $${DATE}, OS: $${OS_NAME}, Uptime: $${UPTIME}" + nginx: command: "docker run -d --rm -p80:80 --name nginx_test nginx" # availability: diff --git a/src/app/project_runner.go b/src/app/project_runner.go index ba1b6b0..7d02404 100644 --- a/src/app/project_runner.go +++ b/src/app/project_runner.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/f1bonacc1/process-compose/src/command" "github.com/f1bonacc1/process-compose/src/config" "github.com/f1bonacc1/process-compose/src/health" "github.com/f1bonacc1/process-compose/src/loader" @@ -15,6 +16,7 @@ import ( "os/user" "runtime" "slices" + "strings" "sync" "time" @@ -90,6 +92,7 @@ func (p *ProjectRunner) Run() error { p.logger.Open(p.project.LogLocation, p.project.LoggerConfig) defer p.logger.Close() } + p.prepareEnvCmds() //zerolog.SetGlobalLevel(zerolog.PanicLevel) log.Debug().Msgf("Spinning up %d processes. Order: %q", len(runOrder), nameOrder) for _, proc := range runOrder { @@ -1046,6 +1049,33 @@ func (p *ProjectRunner) UpdateProcess(updated *types.ProcessConfig) error { return nil } +func (p *ProjectRunner) prepareEnvCmds() { + for env, cmd := range p.project.EnvCommands { + output, err := runCmd(cmd) + if err != nil { + log.Err(err).Msgf("Failed to run Env command %s for %s variable", cmd, env) + continue + } + if p.project.Environment == nil { + p.project.Environment = make(types.Environment, 0) + } + p.project.Environment = append(p.project.Environment, fmt.Sprintf("%s=%s", env, output)) + } +} + +func runCmd(envCmd string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + cmd := command.BuildCommandContext(ctx, envCmd) + out, err := cmd.Output() + if err != nil { + log.Err(err).Msgf("Failed to run Env command %s", envCmd) + return "", err + } + return strings.TrimSpace(string(out)), nil +} + func validateProbes(probe *health.Probe) { if probe != nil { probe.ValidateAndSetDefaults() diff --git a/src/app/system_test.go b/src/app/system_test.go index f68be09..84bce43 100644 --- a/src/app/system_test.go +++ b/src/app/system_test.go @@ -1079,3 +1079,46 @@ func TestSystem_WaitForStartShutDown(t *testing.T) { t.Fatalf("Failed to stop project: %v", err) } } + +func TestSystem_TestEnvCmds(t *testing.T) { + proc1 := "proc1" + shell := command.DefaultShellConfig() + project := &types.Project{ + EnvCommands: map[string]string{ + "LIVE": "echo live", + "LONG": "echo long", + "PROSPER": "echo prosper", + }, + Processes: map[string]types.ProcessConfig{ + proc1: { + Name: proc1, + ReplicaName: proc1, + Executable: shell.ShellCommand, + Args: []string{shell.ShellArgument, "echo $LIVE $LONG and $PROSPER"}, + }, + }, + ShellConfig: shell, + } + runner, err := NewProjectRunner(&ProjectOpts{ + project: project, + }) + if err != nil { + t.Error(err.Error()) + return + } + err = runner.Run() + if err != nil { + t.Error(err.Error()) + } + log, err := runner.GetProcessLog(proc1, 1, 1) + if err != nil { + t.Error(err.Error()) + return + } + if len(log) != 1 { + t.Fatalf("Expected 1 log message, got %d", len(log)) + } + if log[0] != "live long and prosper" { + t.Errorf("Expected log message to be 'live long and prosper', got %s", log[0]) + } +} diff --git a/src/command/command_wrapper.go b/src/command/command_wrapper.go index 7ec1cca..8020477 100644 --- a/src/command/command_wrapper.go +++ b/src/command/command_wrapper.go @@ -55,3 +55,7 @@ func (c *CmdWrapper) SetEnv(env []string) { func (c *CmdWrapper) SetDir(dir string) { c.cmd.Dir = dir } + +func (c *CmdWrapper) Output() ([]byte, error) { + return c.cmd.Output() +} diff --git a/src/command/commander.go b/src/command/commander.go index 6ddbf88..ae051cc 100644 --- a/src/command/commander.go +++ b/src/command/commander.go @@ -16,4 +16,5 @@ type Commander interface { AttachIo() SetEnv(env []string) SetDir(dir string) + Output() ([]byte, error) } diff --git a/src/command/mock_command.go b/src/command/mock_command.go index a4894ea..85dccb6 100644 --- a/src/command/mock_command.go +++ b/src/command/mock_command.go @@ -66,3 +66,7 @@ func (c *MockCommand) SetEnv(env []string) { func (c *MockCommand) SetDir(dir string) { c.dir = dir } + +func (c *MockCommand) Output() ([]byte, error) { + return nil, nil +} diff --git a/src/types/process.go b/src/types/process.go index 843b60d..33b5c09 100644 --- a/src/types/process.go +++ b/src/types/process.go @@ -16,40 +16,43 @@ const DefaultNamespace = "default" const PlaceHolderValue = "-" const DefaultLaunchTimeout = 5 -type Processes map[string]ProcessConfig -type Environment []string -type ProcessConfig struct { - Name string - Disabled bool `yaml:"disabled,omitempty"` - IsDaemon bool `yaml:"is_daemon,omitempty"` - Command string `yaml:"command"` - Entrypoint []string `yaml:"entrypoint,omitempty"` - LogLocation string `yaml:"log_location,omitempty"` - LoggerConfig *LoggerConfig `yaml:"log_configuration,omitempty"` - Environment Environment `yaml:"environment,omitempty"` - RestartPolicy RestartPolicyConfig `yaml:"availability,omitempty"` - DependsOn DependsOnConfig `yaml:"depends_on,omitempty"` - LivenessProbe *health.Probe `yaml:"liveness_probe,omitempty"` - ReadinessProbe *health.Probe `yaml:"readiness_probe,omitempty"` - ReadyLogLine string `yaml:"ready_log_line,omitempty"` - ShutDownParams ShutDownParams `yaml:"shutdown,omitempty"` - DisableAnsiColors bool `yaml:"disable_ansi_colors,omitempty"` - WorkingDir string `yaml:"working_dir"` - Namespace string `yaml:"namespace"` - Replicas int `yaml:"replicas"` - Extensions map[string]interface{} `yaml:",inline"` - Description string `yaml:"description,omitempty"` - Vars Vars `yaml:"vars,omitempty"` - IsForeground bool `yaml:"is_foreground"` - IsTty bool `yaml:"is_tty"` - IsElevated bool `yaml:"is_elevated"` - LaunchTimeout int `yaml:"launch_timeout_seconds"` - OriginalConfig string - ReplicaNum int - ReplicaName string - Executable string - Args []string -} +type ( + Processes map[string]ProcessConfig + Environment []string + EnvCmd map[string]string + ProcessConfig struct { + Name string + Disabled bool `yaml:"disabled,omitempty"` + IsDaemon bool `yaml:"is_daemon,omitempty"` + Command string `yaml:"command"` + Entrypoint []string `yaml:"entrypoint,omitempty"` + LogLocation string `yaml:"log_location,omitempty"` + LoggerConfig *LoggerConfig `yaml:"log_configuration,omitempty"` + Environment Environment `yaml:"environment,omitempty"` + RestartPolicy RestartPolicyConfig `yaml:"availability,omitempty"` + DependsOn DependsOnConfig `yaml:"depends_on,omitempty"` + LivenessProbe *health.Probe `yaml:"liveness_probe,omitempty"` + ReadinessProbe *health.Probe `yaml:"readiness_probe,omitempty"` + ReadyLogLine string `yaml:"ready_log_line,omitempty"` + ShutDownParams ShutDownParams `yaml:"shutdown,omitempty"` + DisableAnsiColors bool `yaml:"disable_ansi_colors,omitempty"` + WorkingDir string `yaml:"working_dir"` + Namespace string `yaml:"namespace"` + Replicas int `yaml:"replicas"` + Extensions map[string]interface{} `yaml:",inline"` + Description string `yaml:"description,omitempty"` + Vars Vars `yaml:"vars,omitempty"` + IsForeground bool `yaml:"is_foreground"` + IsTty bool `yaml:"is_tty"` + IsElevated bool `yaml:"is_elevated"` + LaunchTimeout int `yaml:"launch_timeout_seconds"` + OriginalConfig string + ReplicaNum int + ReplicaName string + Executable string + Args []string + } +) func (p *ProcessConfig) GetDependencies() []string { dependencies := make([]string, len(p.DependsOn)) diff --git a/src/types/project.go b/src/types/project.go index 8aef56d..d0c1005 100644 --- a/src/types/project.go +++ b/src/types/project.go @@ -23,6 +23,7 @@ type Project struct { DisableEnvExpansion bool `yaml:"disable_env_expansion"` IsTuiDisabled bool `yaml:"is_tui_disabled"` ExtendsProject string `yaml:"extends,omitempty"` + EnvCommands EnvCmd `yaml:"env_cmds,omitempty"` FileNames []string EnvFileNames []string } diff --git a/www/docs/configuration.md b/www/docs/configuration.md index 9b15e8e..3da2375 100644 --- a/www/docs/configuration.md +++ b/www/docs/configuration.md @@ -107,6 +107,83 @@ Process Compose provides 2 ways to disable the automatic environment variables e > > **Output**: `I am ` +## Environment Commands + +The `env_cmds` feature allows you to dynamically populate environment variables by executing short commands before starting your processes. This is useful when you need environment values that are determined at runtime or need to be fetched from the system. + +### Configuration + +Environment commands are defined in the `env_cmds` section of your `process-compose.yaml` file. Each entry consists of: +- An environment variable name (key) +- A command to execute (value) + +```yaml +env_cmds: + ENV_VAR_NAME: "command to execute" +``` + +### Example Configuration + +```yaml +env_cmds: + DATE: "date" + OS_NAME: "awk -F= '/PRETTY/ {print $2}' /etc/os-release" + UPTIME: "uptime -p" +``` + +### Usage + +To use the environment variables populated by `env_cmds`, reference them in your process definitions using `$${VAR_NAME}` syntax: + +```yaml +processes: + my-process: + command: "echo Current date is: $${DATE}" +``` + +### Constraints and Considerations + +1. **Execution Time**: Commands should complete within 2 seconds. Longer-running commands may cause process-compose startup delays or timeouts. + +2. **Command Output**: + - Commands should output a single line of text + - The output will be trimmed of leading/trailing whitespace + - The output becomes the value of the environment variable + +3. **Error Handling**: + - If a command fails, the environment variable will not be set + - Process-compose will log any command execution errors + +### Best Practices + +1. Keep commands simple and fast-executing +2. Use commands that produce consistent, predictable output +3. Validate command output format before using in production +4. Consider caching values that don't need frequent updates + +### Example Use Cases + +1. **System Information**: +```yaml +env_cmds: + HOSTNAME: "hostname" + KERNEL_VERSION: "uname -r" +``` + +2. **Time-based Values**: +```yaml +env_cmds: + TIMESTAMP: "date +%s" + DATE_ISO: "date -u +%Y-%m-%dT%H:%M:%SZ" +``` + +3. **Resource Information**: +```yaml +env_cmds: + AVAILABLE_MEMORY: "free -m | awk '/Mem:/ {print $7}'" + CPU_CORES: "nproc" +``` + ## Variables Variables in Process Compose rely on [Go template engine](https://pkg.go.dev/text/template)