Skip to content

Commit

Permalink
feat: Add Environment Commands
Browse files Browse the repository at this point in the history
  • Loading branch information
F1bonacc1 committed Dec 20, 2024
1 parent d7e0380 commit 05fa613
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 34 deletions.
7 changes: 7 additions & 0 deletions process-compose.override.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
30 changes: 30 additions & 0 deletions src/app/project_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -15,6 +16,7 @@ import (
"os/user"
"runtime"
"slices"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
43 changes: 43 additions & 0 deletions src/app/system_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
}
4 changes: 4 additions & 0 deletions src/command/command_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
1 change: 1 addition & 0 deletions src/command/commander.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ type Commander interface {
AttachIo()
SetEnv(env []string)
SetDir(dir string)
Output() ([]byte, error)
}
4 changes: 4 additions & 0 deletions src/command/mock_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
71 changes: 37 additions & 34 deletions src/types/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions src/types/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
77 changes: 77 additions & 0 deletions www/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 05fa613

Please sign in to comment.