From 05fa613cae334949b6511d1c9adcae1dc40cefb7 Mon Sep 17 00:00:00 2001 From: Berger Eugene Date: Fri, 20 Dec 2024 23:02:29 +0200 Subject: [PATCH] feat: Add Environment Commands --- process-compose.override.yaml | 7 ++++ src/app/project_runner.go | 30 +++++++++++++ src/app/system_test.go | 43 +++++++++++++++++++ src/command/command_wrapper.go | 4 ++ src/command/commander.go | 1 + src/command/mock_command.go | 4 ++ src/types/process.go | 71 ++++++++++++++++--------------- src/types/project.go | 1 + www/docs/configuration.md | 77 ++++++++++++++++++++++++++++++++++ 9 files changed, 204 insertions(+), 34 deletions(-) diff --git a/process-compose.override.yaml b/process-compose.override.yaml index aa4da7f2..2624e2ac 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 ba1b6b0c..7d024040 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 f68be09c..84bce43c 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 7ec1ccab..80204776 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 6ddbf88e..ae051cc1 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 a4894ea4..85dccb65 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 843b60da..33b5c092 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 8aef56df..d0c1005c 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 9b15e8e4..3da2375f 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)