Merge 2 or more configuration files

This commit is contained in:
Berger Eugene 2023-01-22 21:15:04 +02:00
parent a1515562b7
commit e064219f92
29 changed files with 1674 additions and 740 deletions

View File

@ -52,7 +52,7 @@ testrace:
go test -race ./src/...
coverhtml:
go test -coverprofile=coverage.out ./src
go test -coverprofile=coverage.out ./src/...
go tool cover -html=coverage.out
run:

2
go.mod
View File

@ -9,7 +9,9 @@ require (
github.com/fatih/color v1.13.0
github.com/gdamore/tcell/v2 v2.5.3
github.com/gin-gonic/gin v1.8.1
github.com/imdario/mergo v0.3.13
github.com/joho/godotenv v1.4.0
github.com/pkg/errors v0.9.1
github.com/rivo/tview v0.0.0-20221128165837-db36428c92d9
github.com/spf13/cobra v1.6.1
github.com/swaggo/swag v1.8.8

4
go.sum
View File

@ -81,6 +81,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
@ -142,6 +144,7 @@ github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZO
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -282,5 +285,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,132 @@
version: "0.5"
log_level: debug
log_length: 3000
environment:
- 'ABC=222'
log_location: ./pc.log
shell:
shell_command: "zsh"
shell_argument: "-c"
processes:
process0:
command: "ls -lFa --color=always"
working_dir: "/"
process1:
command: "./test_loop.bash ${PROC4}"
availability:
restart: "on_failure"
backoff_seconds: 2
depends_on:
_process2:
condition: process_completed_successfully
process3:
condition: process_completed
# process4:
# condition: process_completed_successfully
environment:
- 'EXIT_CODE=0'
shutdown:
command: "sleep 2 && pkill -f process1"
signal: 15
timeout_seconds: 4
_process2:
command: "./test_loop.bash process2"
log_location: ./pc.proc2.log
availability:
restart: "on_failure"
# depends_on:
# process3:
# condition: process_completed_successfully
environment:
- 'ABC=2221'
- 'PRINT_ERR=111'
- 'EXIT_CODE=2'
shutdown:
command: "sleep 2 && pkill -f 'test_loop.bash process2'"
signal: 15
timeout_seconds: 4
readiness_probe:
http_get:
host: "google.com"
scheme: "https"
initial_delay_seconds: 5
period_seconds: 5
timeout_seconds: 2
success_threshold: 1
failure_threshold: 3
process3:
command: "./test_loop.bash process3"
availability:
restart: "always"
backoff_seconds: 2
depends_on:
nginx:
condition: process_healthy
process4:
command: "./test_loop.bash process4-override"
disable_ansi_colors: true
# availability:
# restart: on_failure
environment:
- 'ABC=2221'
- 'EXIT_CODE=4'
readiness_probe:
exec:
command: "ps -ef | grep -v grep | grep process4"
initial_delay_seconds: 5
period_seconds: 2
timeout_seconds: 1
success_threshold: 1
failure_threshold: 3
nginx:
command: "docker run -d --rm -p80:80 --name nginx_test nginx"
# availability:
# restart: on_failure
is_daemon: true
shutdown:
command: "docker stop nginx_test"
signal: 15
timeout_seconds: 5
liveness_probe:
exec:
command: "[ $(docker inspect -f '{{.State.Running}}' nginx_test) = 'true' ]"
initial_delay_seconds: 5
period_seconds: 2
timeout_seconds: 5
success_threshold: 1
failure_threshold: 3
readiness_probe:
http_get:
host: 127.0.0.1
path: "/"
port: 80
initial_delay_seconds: 5
period_seconds: 10
timeout_seconds: 5
success_threshold: 1
failure_threshold: 3
availability:
restart: "always"
backoff_seconds: 2
pc_log:
command: "tail -f -n100 process-compose-${USER}.log"
working_dir: "/tmp"
environment:
- 'REDACTED=1'
- 'ADDED=2'
depends_on:
process0:
condition: process_completed
bat_config:
command: "batcat -f process-compose.yaml"

View File

@ -1,6 +1,13 @@
version: "0.5"
log_level: debug
log_level: info
log_length: 3000
environment:
- 'ABC=222'
log_location: ./pc.log
shell:
shell_command: "zsh"
shell_argument: "-c"
processes:
process0:
command: "ls -lFa --color=always"
@ -77,36 +84,36 @@ processes:
success_threshold: 1
failure_threshold: 3
nginx:
command: "docker run -d --rm -p80:80 --name nginx_test nginx"
# availability:
# restart: on_failure
is_daemon: true
shutdown:
command: "docker stop nginx_test"
signal: 15
timeout_seconds: 5
liveness_probe:
exec:
command: "[ $(docker inspect -f '{{.State.Running}}' nginx_test) = 'true' ]"
initial_delay_seconds: 5
period_seconds: 2
timeout_seconds: 5
success_threshold: 1
failure_threshold: 3
readiness_probe:
http_get:
host: 127.0.0.1
path: "/"
port: 80
initial_delay_seconds: 5
period_seconds: 10
timeout_seconds: 5
success_threshold: 1
failure_threshold: 3
availability:
restart: "always"
backoff_seconds: 2
# nginx:
# command: "docker run -d --rm -p80:80 --name nginx_test nginx"
# # availability:
# # restart: on_failure
# is_daemon: true
# shutdown:
# command: "docker stop nginx_test"
# signal: 15
# timeout_seconds: 5
# liveness_probe:
# exec:
# command: "[ $(docker inspect -f '{{.State.Running}}' nginx_test) = 'true' ]"
# initial_delay_seconds: 5
# period_seconds: 2
# timeout_seconds: 5
# success_threshold: 1
# failure_threshold: 3
# readiness_probe:
# http_get:
# host: 127.0.0.1
# path: "/"
# port: 80
# initial_delay_seconds: 5
# period_seconds: 10
# timeout_seconds: 5
# success_threshold: 1
# failure_threshold: 3
# availability:
# restart: "always"
# backoff_seconds: 2
kcalc:
command: "kcalc"
@ -125,9 +132,3 @@ processes:
bat_config:
command: "batcat -f process-compose.yaml"
environment:
- 'ABC=222'
log_location: ./pc.log
shell:
shell_command: "zsh"
shell_argument: "-c"

View File

@ -1,6 +1,7 @@
package api
import (
"github.com/f1bonacc1/process-compose/src/types"
"net/http"
"strconv"
@ -17,9 +18,9 @@ import (
// @Success 200 {object} object "Processes Status"
// @Router /processes [get]
func GetProcesses(c *gin.Context) {
procs := app.PROJ.GetLexicographicProcessNames()
procs := app.PROJ.GetProject().GetLexicographicProcessNames()
states := []*app.ProcessState{}
states := []*types.ProcessState{}
for _, name := range procs {
states = append(states, app.PROJ.GetProcessState(name))
}

View File

@ -1,5 +1,7 @@
package app
import "github.com/f1bonacc1/process-compose/src/types"
func (p *Process) waitForDaemonCompletion() {
if !p.isDaemonLaunched() {
return
@ -9,7 +11,7 @@ loop:
for {
status := <-p.procStateChan
switch status {
case ProcessStateCompleted:
case types.ProcessStateCompleted:
break loop
}
}
@ -18,7 +20,7 @@ loop:
func (p *Process) notifyDaemonStopped() {
if p.isDaemonLaunched() {
p.procStateChan <- ProcessStateCompleted
p.procStateChan <- types.ProcessStateCompleted
}
}

View File

@ -4,6 +4,7 @@ import (
"bufio"
"context"
"fmt"
"github.com/f1bonacc1/process-compose/src/types"
"io"
"math/rand"
"os"
@ -30,8 +31,8 @@ type Process struct {
sync.Mutex
globalEnv []string
procConf ProcessConfig
procState *ProcessState
procConf types.ProcessConfig
procState *types.ProcessState
stateMtx sync.Mutex
procCond sync.Cond
procStateChan chan string
@ -55,8 +56,8 @@ type Process struct {
func NewProcess(
globalEnv []string,
logger pclog.PcLogger,
procConf ProcessConfig,
procState *ProcessState,
procConf types.ProcessConfig,
procState *types.ProcessState,
procLog *pclog.ProcessLogBuffer,
replica int,
shellConfig command.ShellConfig) *Process {
@ -85,13 +86,13 @@ func NewProcess(
}
func (p *Process) run() int {
if p.isState(ProcessStateTerminating) {
if p.isState(types.ProcessStateTerminating) {
return 0
}
if err := p.validateProcess(); err != nil {
log.Error().Err(err).Msgf("Failed to run command %s for process %s", p.getCommand(), p.getName())
p.onProcessEnd(ProcessStateError)
p.onProcessEnd(types.ProcessStateError)
return 1
}
@ -100,7 +101,7 @@ func (p *Process) run() int {
if err != nil {
log.Error().Err(err).Msgf("Failed to run command %s for process %s", p.getCommand(), p.getName())
p.logBuffer.Write(err.Error())
p.onProcessEnd(ProcessStateError)
p.onProcessEnd(types.ProcessStateError)
return 1
}
@ -121,21 +122,21 @@ func (p *Process) run() int {
log.Info().Msgf("%s exited with status %d", p.getName(), p.procState.ExitCode)
if p.isDaemonLaunched() {
p.setState(ProcessStateLaunched)
p.setState(types.ProcessStateLaunched)
p.waitForDaemonCompletion()
}
if !p.isRestartable() {
break
}
p.setState(ProcessStateRestarting)
p.setState(types.ProcessStateRestarting)
p.procState.Restarts += 1
log.Info().Msgf("Restarting %s in %v second(s)... Restarts: %d",
p.procConf.Name, p.getBackoff().Seconds(), p.procState.Restarts)
time.Sleep(p.getBackoff())
}
p.onProcessEnd(ProcessStateCompleted)
p.onProcessEnd(types.ProcessStateCompleted)
return p.procState.ExitCode
}
@ -174,17 +175,17 @@ func (p *Process) getProcessEnvironment() []string {
func (p *Process) isRestartable() bool {
exitCode := p.procState.ExitCode
if p.procConf.RestartPolicy.Restart == RestartPolicyNo ||
if p.procConf.RestartPolicy.Restart == types.RestartPolicyNo ||
p.procConf.RestartPolicy.Restart == "" {
return false
}
if exitCode != 0 && p.procConf.RestartPolicy.Restart == RestartPolicyExitOnFailure {
if exitCode != 0 && p.procConf.RestartPolicy.Restart == types.RestartPolicyExitOnFailure {
return false
}
if exitCode != 0 && (p.procConf.RestartPolicy.Restart == RestartPolicyOnFailureDeprecated ||
p.procConf.RestartPolicy.Restart == RestartPolicyOnFailure) {
if exitCode != 0 && (p.procConf.RestartPolicy.Restart == types.RestartPolicyOnFailureDeprecated ||
p.procConf.RestartPolicy.Restart == types.RestartPolicyOnFailure) {
if p.procConf.RestartPolicy.MaxRestarts == 0 {
return true
}
@ -192,7 +193,7 @@ func (p *Process) isRestartable() bool {
}
// TODO consider if forking daemon should disable RestartPolicyAlways
if p.procConf.RestartPolicy.Restart == RestartPolicyAlways {
if p.procConf.RestartPolicy.Restart == types.RestartPolicyAlways {
if p.procConf.RestartPolicy.MaxRestarts == 0 {
return true
}
@ -219,7 +220,7 @@ func (p *Process) waitUntilReady() bool {
log.Error().Msgf("Process %s was aborted and won't become ready", p.getName())
return false
case ready := <-p.procReadyChan:
if ready == ProcessHealthReady {
if ready == types.ProcessHealthReady {
return true
}
}
@ -227,7 +228,7 @@ func (p *Process) waitUntilReady() bool {
}
func (p *Process) wontRun() {
p.onProcessEnd(ProcessStateCompleted)
p.onProcessEnd(types.ProcessStateCompleted)
}
@ -236,10 +237,10 @@ func (p *Process) shutDown() error {
if !p.isRunning() {
log.Debug().Msgf("process %s is in state %s not shutting down", p.getName(), p.procState.Status)
// prevent pending process from running
p.onProcessEnd(ProcessStateTerminating)
p.onProcessEnd(types.ProcessStateTerminating)
return nil
}
p.setState(ProcessStateTerminating)
p.setState(types.ProcessStateTerminating)
p.stopProbes()
if isStringDefined(p.procConf.ShutDownParams.ShutDownCommand) {
return p.doConfiguredStop(p.procConf.ShutDownParams)
@ -247,7 +248,7 @@ func (p *Process) shutDown() error {
return p.stop(p.procConf.ShutDownParams.Signal)
}
func (p *Process) doConfiguredStop(params ShutDownParams) error {
func (p *Process) doConfiguredStop(params types.ShutDownParams) error {
timeout := params.ShutDownTimeout
if timeout == UndefinedShutdownTimeoutSec {
timeout = DefaultShutdownTimeoutSec
@ -269,12 +270,12 @@ func (p *Process) doConfiguredStop(params ShutDownParams) error {
}
func (p *Process) isRunning() bool {
return p.isOneOfStates(ProcessStateRunning, ProcessStateLaunched)
return p.isOneOfStates(types.ProcessStateRunning, types.ProcessStateLaunched)
}
func (p *Process) prepareForShutDown() {
// prevent restart during global shutdown
p.procConf.RestartPolicy.Restart = RestartPolicyNo
p.procConf.RestartPolicy.Restart = types.RestartPolicyNo
}
func (p *Process) onProcessEnd(state string) {
@ -366,20 +367,20 @@ func (p *Process) setStateAndRun(state string, runnable func() error) error {
func (p *Process) onStateChange(state string) {
switch state {
case ProcessStateRestarting:
case types.ProcessStateRestarting:
fallthrough
case ProcessStateLaunching:
case types.ProcessStateLaunching:
fallthrough
case ProcessStateTerminating:
p.procState.Health = ProcessHealthUnknown
case types.ProcessStateTerminating:
p.procState.Health = types.ProcessHealthUnknown
}
}
func (p *Process) getStartingStateName() string {
if p.procConf.IsDaemon {
return ProcessStateLaunching
return types.ProcessStateLaunching
}
return ProcessStateRunning
return types.ProcessStateRunning
}
func (p *Process) setUpProbes() {
@ -440,15 +441,15 @@ func (p *Process) onLivenessCheckEnd(_, isFatal bool, err string) {
func (p *Process) onReadinessCheckEnd(isOk, isFatal bool, err string) {
if isFatal {
p.procState.Health = ProcessHealthNotReady
p.procState.Health = types.ProcessHealthNotReady
log.Info().Msgf("%s is not ready anymore - %s", p.getName(), err)
p.logBuffer.Write("Error: readiness check fail - " + err)
_ = p.shutDown()
} else if isOk {
p.procState.Health = ProcessHealthReady
p.procReadyChan <- ProcessHealthReady
p.procState.Health = types.ProcessHealthReady
p.procReadyChan <- types.ProcessHealthReady
} else {
p.procState.Health = ProcessHealthNotReady
p.procState.Health = types.ProcessHealthNotReady
}
}

View File

@ -1,519 +0,0 @@
package app
import (
"errors"
"fmt"
"github.com/f1bonacc1/process-compose/src/command"
"github.com/f1bonacc1/process-compose/src/pclog"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/joho/godotenv"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
)
const (
DEFAULT_LOG_LENGTH = 1000
)
var PROJ *Project
func (p *Project) init() {
p.setConfigDefaults()
p.initProcessStates()
p.initProcessLogs()
p.deprecationCheck()
p.validateProcessConfig()
}
func (p *Project) Run() int {
p.runningProcesses = make(map[string]*Process)
runOrder := []ProcessConfig{}
_ = p.WithProcesses([]string{}, func(process ProcessConfig) error {
runOrder = append(runOrder, process)
return nil
})
var nameOrder []string
for _, v := range runOrder {
nameOrder = append(nameOrder, v.Name)
}
p.logger = pclog.NewNilLogger()
if isStringDefined(p.LogLocation) {
p.logger = pclog.NewLogger(p.LogLocation)
defer p.logger.Close()
}
//zerolog.SetGlobalLevel(zerolog.PanicLevel)
log.Debug().Msgf("Spinning up %d processes. Order: %q", len(runOrder), nameOrder)
for _, proc := range runOrder {
p.runProcess(proc)
}
p.wg.Wait()
log.Info().Msg("Project completed")
return p.exitCode
}
func (p *Project) runProcess(proc ProcessConfig) {
procLogger := p.logger
if isStringDefined(proc.LogLocation) {
procLogger = pclog.NewLogger(proc.LogLocation)
}
procLog, err := p.getProcessLog(proc.Name)
if err != nil {
// we shouldn't get here
log.Error().Msgf("Error: Can't get log: %s using empty buffer", err.Error())
procLog = pclog.NewLogBuffer(0)
}
process := NewProcess(p.Environment, procLogger, proc, p.GetProcessState(proc.Name), procLog, 1, *p.ShellConfig)
p.addRunningProcess(process)
p.wg.Add(1)
go func() {
defer p.removeRunningProcess(process.getName())
defer p.wg.Done()
if err := p.waitIfNeeded(process.procConf); err != nil {
log.Error().Msgf("Error: %s", err.Error())
log.Error().Msgf("Error: process %s won't run", process.getName())
process.wontRun()
} else {
exitCode := process.run()
p.onProcessEnd(exitCode, process.procConf)
}
}()
}
func (p *Project) waitIfNeeded(process ProcessConfig) error {
for k := range process.DependsOn {
if runningProc := p.getRunningProcess(k); runningProc != nil {
switch process.DependsOn[k].Condition {
case ProcessConditionCompleted:
runningProc.waitForCompletion()
case ProcessConditionCompletedSuccessfully:
log.Info().Msgf("%s is waiting for %s to complete successfully", process.Name, k)
exitCode := runningProc.waitForCompletion()
if exitCode != 0 {
return fmt.Errorf("process %s depended on %s to complete successfully, but it exited with status %d",
process.Name, k, exitCode)
}
case ProcessConditionHealthy:
log.Info().Msgf("%s is waiting for %s to be healthy", process.Name, k)
ready := runningProc.waitUntilReady()
if !ready {
return fmt.Errorf("process %s depended on %s to become ready, but it was terminated", process.Name, k)
}
}
}
}
return nil
}
func (p *Project) onProcessEnd(exitCode int, procConf ProcessConfig) {
if exitCode != 0 && procConf.RestartPolicy.Restart == RestartPolicyExitOnFailure {
p.ShutDownProject()
p.exitCode = exitCode
}
}
func (p *Project) initProcessStates() {
p.processStates = make(map[string]*ProcessState)
for key, proc := range p.Processes {
p.processStates[key] = &ProcessState{
Name: key,
Status: ProcessStatePending,
SystemTime: "",
Health: ProcessHealthUnknown,
Restarts: 0,
ExitCode: 0,
Pid: 0,
}
if proc.Disabled {
p.processStates[key].Status = ProcessStateDisabled
}
}
}
func (p *Project) initProcessLogs() {
p.processLogs = make(map[string]*pclog.ProcessLogBuffer)
for key := range p.Processes {
p.processLogs[key] = pclog.NewLogBuffer(p.LogLength)
}
}
func (p *Project) deprecationCheck() {
for key, proc := range p.Processes {
if proc.RestartPolicy.Restart == RestartPolicyOnFailureDeprecated {
deprecationHandler("2022-10-30", key, RestartPolicyOnFailureDeprecated, RestartPolicyOnFailure, "restart policy")
}
}
}
func (p *Project) setConfigDefaults() {
if p.ShellConfig == nil {
p.ShellConfig = command.DefaultShellConfig()
}
log.Info().Msgf("Global shell command: %s %s", p.ShellConfig.ShellCommand, p.ShellConfig.ShellArgument)
command.ValidateShellConfig(*p.ShellConfig)
}
func (p *Project) validateProcessConfig() {
for key, proc := range p.Processes {
if len(proc.Extensions) == 0 {
continue
}
for extKey := range proc.Extensions {
if strings.HasPrefix(extKey, "x-") {
continue
}
log.Error().Msgf("Unknown key %s found in process %s", extKey, key)
}
}
}
func (p *Project) GetProcessState(name string) *ProcessState {
if procState, ok := p.processStates[name]; ok {
proc := p.getRunningProcess(name)
if proc != nil {
proc.updateProcState()
} else {
procState.Pid = 0
procState.SystemTime = ""
procState.Health = ProcessHealthUnknown
procState.IsRunning = false
}
return procState
}
log.Error().Msgf("Error: process %s doesn't exist", name)
return nil
}
func (p *Project) addRunningProcess(process *Process) {
p.mapMutex.Lock()
p.runningProcesses[process.getName()] = process
p.mapMutex.Unlock()
}
func (p *Project) getRunningProcess(name string) *Process {
p.mapMutex.Lock()
defer p.mapMutex.Unlock()
if runningProc, ok := p.runningProcesses[name]; ok {
return runningProc
}
return nil
}
func (p *Project) removeRunningProcess(name string) {
p.mapMutex.Lock()
delete(p.runningProcesses, name)
p.mapMutex.Unlock()
}
func (p *Project) StartProcess(name string) error {
proc := p.getRunningProcess(name)
if proc != nil {
log.Error().Msgf("Process %s is already running", name)
return fmt.Errorf("process %s is already running", name)
}
if processConfig, ok := p.Processes[name]; ok {
processConfig.Name = name
p.runProcess(processConfig)
} else {
return fmt.Errorf("no such process: %s", name)
}
return nil
}
func (p *Project) StopProcess(name string) error {
proc := p.getRunningProcess(name)
if proc == nil {
log.Error().Msgf("Process %s is not running", name)
return fmt.Errorf("process %s is not running", name)
}
_ = proc.shutDown()
return nil
}
func (p *Project) RestartProcess(name string) error {
proc := p.getRunningProcess(name)
if proc != nil {
_ = proc.shutDown()
if proc.isRestartable() {
return nil
}
time.Sleep(proc.getBackoff())
}
if processConfig, ok := p.Processes[name]; ok {
processConfig.Name = name
p.runProcess(processConfig)
} else {
return fmt.Errorf("no such process: %s", name)
}
return nil
}
func (p *Project) GetProcessInfo(name string) (*ProcessConfig, error) {
if processConfig, ok := p.Processes[name]; ok {
processConfig.Name = name
return &processConfig, nil
} else {
return nil, fmt.Errorf("no such process: %s", name)
}
}
func (p *Project) ShutDownProject() {
p.mapMutex.Lock()
defer p.mapMutex.Unlock()
runProc := p.runningProcesses
for _, proc := range runProc {
proc.prepareForShutDown()
}
for _, proc := range runProc {
_ = proc.shutDown()
}
}
func (p *Project) getProcessLog(name string) (*pclog.ProcessLogBuffer, error) {
if procLogs, ok := p.processLogs[name]; ok {
return procLogs, nil
}
log.Error().Msgf("Error: process %s doesn't exist", name)
return nil, fmt.Errorf("process %s doesn't exist", name)
}
func (p *Project) GetProcessLog(name string, offsetFromEnd, limit int) ([]string, error) {
logs, err := p.getProcessLog(name)
if err != nil {
return nil, err
}
return logs.GetLogRange(offsetFromEnd, limit), nil
}
func (p *Project) GetProcessLogLine(name string, lineIndex int) (string, error) {
logs, err := p.getProcessLog(name)
if err != nil {
return "", err
}
return logs.GetLogLine(lineIndex), nil
}
func (p *Project) GetProcessLogLength(name string) int {
logs, err := p.getProcessLog(name)
if err != nil {
return 0
}
return logs.GetLogLength()
}
func (p *Project) GetLogsAndSubscribe(name string, observer pclog.PcLogObserver) {
logs, err := p.getProcessLog(name)
if err != nil {
return
}
logs.GetLogsAndSubscribe(observer)
}
func (p *Project) UnSubscribeLogger(name string) {
logs, err := p.getProcessLog(name)
if err != nil {
return
}
logs.UnSubscribe()
}
func (p *Project) getProcesses(names ...string) ([]ProcessConfig, error) {
processes := []ProcessConfig{}
if len(names) == 0 {
for name, proc := range p.Processes {
if proc.Disabled {
continue
}
proc.Name = name
processes = append(processes, proc)
}
return processes, nil
}
for _, name := range names {
if proc, ok := p.Processes[name]; ok {
if proc.Disabled {
continue
}
proc.Name = name
processes = append(processes, proc)
} else {
return processes, fmt.Errorf("no such process: %s", name)
}
}
return processes, nil
}
type ProcessFunc func(process ProcessConfig) error
// WithProcesses run ProcesseFunc on each Process and dependencies in dependency order
func (p *Project) WithProcesses(names []string, fn ProcessFunc) error {
return p.withProcesses(names, fn, map[string]bool{})
}
func (p *Project) withProcesses(names []string, fn ProcessFunc, done map[string]bool) error {
processes, err := p.getProcesses(names...)
if err != nil {
return err
}
for _, process := range processes {
if done[process.Name] {
continue
}
done[process.Name] = true
dependencies := process.GetDependencies()
if len(dependencies) > 0 {
err := p.withProcesses(dependencies, fn, done)
if err != nil {
return err
}
}
if err := fn(process); err != nil {
return err
}
}
return nil
}
func (p *Project) GetDependenciesOrderNames() ([]string, error) {
order := []string{}
err := p.WithProcesses([]string{}, func(process ProcessConfig) error {
order = append(order, process.Name)
return nil
})
return order, err
}
func (p *Project) GetLexicographicProcessNames() []string {
names := []string{}
for name := range p.Processes {
names = append(names, name)
}
sort.Strings(names)
return names
}
func (p *Project) selectRunningProcesses(procList []string) error {
if len(procList) == 0 {
return nil
}
newProcMap := Processes{}
err := p.WithProcesses(procList, func(process ProcessConfig) error {
newProcMap[process.Name] = process
return nil
})
if err != nil {
log.Err(err).Msgf("Failed select processes")
return err
}
p.Processes = newProcMap
return nil
}
func (p *Project) selectRunningProcessesNoDeps(procList []string) error {
if len(procList) == 0 {
return nil
}
newProcMap := Processes{}
for _, procName := range procList {
if conf, ok := p.Processes[procName]; ok {
conf.DependsOn = DependsOnConfig{}
newProcMap[procName] = conf
} else {
err := fmt.Errorf("no such process: %s", procName)
log.Err(err).Msgf("Failed select processes")
return err
}
}
p.Processes = newProcMap
return nil
}
func NewProject(inputFile string, processesToRun []string, noDeps bool) (*Project, error) {
yamlFile, err := os.ReadFile(inputFile)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
log.Error().Msgf("File %s doesn't exist", inputFile)
}
log.Fatal().Msg(err.Error())
}
// .env is optional we don't care if it errors
_ = godotenv.Load()
yamlFile = []byte(os.ExpandEnv(string(yamlFile)))
project := &Project{
LogLength: DEFAULT_LOG_LENGTH,
exitCode: 0,
}
err = yaml.Unmarshal(yamlFile, project)
if err != nil {
fmt.Printf("Failed to parse %s - %s\n", inputFile, err.Error())
log.Fatal().Msgf("Failed to parse %s - %s", inputFile, err.Error())
}
if project.LogLevel != "" {
lvl, err := zerolog.ParseLevel(project.LogLevel)
if err != nil {
log.Warn().Msgf("Unknown log level %s defaulting to %s",
project.LogLevel, zerolog.GlobalLevel().String())
} else {
zerolog.SetGlobalLevel(lvl)
}
}
if noDeps {
err = project.selectRunningProcessesNoDeps(processesToRun)
} else {
err = project.selectRunningProcesses(processesToRun)
}
if err != nil {
return nil, err
}
PROJ = project
project.init()
return project, nil
}
func findFiles(names []string, pwd string) []string {
candidates := []string{}
for _, n := range names {
f := filepath.Join(pwd, n)
if _, err := os.Stat(f); err == nil {
candidates = append(candidates, f)
}
}
return candidates
}
// DefaultFileNames defines the Compose file names for auto-discovery (in order of preference)
var DefaultFileNames = []string{"compose.yml", "compose.yaml", "process-compose.yml", "process-compose.yaml"}
func AutoDiscoverComposeFile(pwd string) (string, error) {
candidates := findFiles(DefaultFileNames, pwd)
if len(candidates) > 0 {
winner := candidates[0]
if len(candidates) > 1 {
log.Warn().Msgf("Found multiple config files with supported names: %s", strings.Join(candidates, ", "))
log.Warn().Msgf("Using %s", winner)
}
return winner, nil
}
return "", fmt.Errorf("no config files found in %s", pwd)
}

364
src/app/project_runner.go Normal file
View File

@ -0,0 +1,364 @@
package app
import (
"fmt"
"github.com/f1bonacc1/process-compose/src/pclog"
"github.com/f1bonacc1/process-compose/src/types"
"sync"
"time"
"github.com/rs/zerolog/log"
)
var PROJ *ProjectRunner
type ProjectRunner struct {
project *types.Project
runningProcesses map[string]*Process
processStates map[string]*types.ProcessState
processLogs map[string]*pclog.ProcessLogBuffer
mapMutex sync.Mutex
logger pclog.PcLogger
waitGroup sync.WaitGroup
exitCode int
}
func (p *ProjectRunner) init() {
p.initProcessStates()
p.initProcessLogs()
}
func (p *ProjectRunner) Run() int {
p.runningProcesses = make(map[string]*Process)
runOrder := []types.ProcessConfig{}
_ = p.project.WithProcesses([]string{}, func(process types.ProcessConfig) error {
runOrder = append(runOrder, process)
return nil
})
var nameOrder []string
for _, v := range runOrder {
nameOrder = append(nameOrder, v.Name)
}
p.logger = pclog.NewNilLogger()
if isStringDefined(p.project.LogLocation) {
p.logger = pclog.NewLogger(p.project.LogLocation)
defer p.logger.Close()
}
//zerolog.SetGlobalLevel(zerolog.PanicLevel)
log.Debug().Msgf("Spinning up %d processes. Order: %q", len(runOrder), nameOrder)
for _, proc := range runOrder {
p.runProcess(proc)
}
p.waitGroup.Wait()
log.Info().Msg("Project completed")
return p.exitCode
}
func (p *ProjectRunner) runProcess(proc types.ProcessConfig) {
procLogger := p.logger
if isStringDefined(proc.LogLocation) {
procLogger = pclog.NewLogger(proc.LogLocation)
}
procLog, err := p.getProcessLog(proc.Name)
if err != nil {
// we shouldn't get here
log.Error().Msgf("Error: Can't get log: %s using empty buffer", err.Error())
procLog = pclog.NewLogBuffer(0)
}
process := NewProcess(p.project.Environment, procLogger, proc, p.GetProcessState(proc.Name), procLog, 1, *p.project.ShellConfig)
p.addRunningProcess(process)
p.waitGroup.Add(1)
go func() {
defer p.removeRunningProcess(process.getName())
defer p.waitGroup.Done()
if err := p.waitIfNeeded(process.procConf); err != nil {
log.Error().Msgf("Error: %s", err.Error())
log.Error().Msgf("Error: process %s won't run", process.getName())
process.wontRun()
} else {
exitCode := process.run()
p.onProcessEnd(exitCode, process.procConf)
}
}()
}
func (p *ProjectRunner) waitIfNeeded(process types.ProcessConfig) error {
for k := range process.DependsOn {
if runningProc := p.getRunningProcess(k); runningProc != nil {
switch process.DependsOn[k].Condition {
case types.ProcessConditionCompleted:
runningProc.waitForCompletion()
case types.ProcessConditionCompletedSuccessfully:
log.Info().Msgf("%s is waiting for %s to complete successfully", process.Name, k)
exitCode := runningProc.waitForCompletion()
if exitCode != 0 {
return fmt.Errorf("process %s depended on %s to complete successfully, but it exited with status %d",
process.Name, k, exitCode)
}
case types.ProcessConditionHealthy:
log.Info().Msgf("%s is waiting for %s to be healthy", process.Name, k)
ready := runningProc.waitUntilReady()
if !ready {
return fmt.Errorf("process %s depended on %s to become ready, but it was terminated", process.Name, k)
}
}
}
}
return nil
}
func (p *ProjectRunner) onProcessEnd(exitCode int, procConf types.ProcessConfig) {
if exitCode != 0 && procConf.RestartPolicy.Restart == types.RestartPolicyExitOnFailure {
p.ShutDownProject()
p.exitCode = exitCode
}
}
func (p *ProjectRunner) initProcessStates() {
p.processStates = make(map[string]*types.ProcessState)
for key, proc := range p.project.Processes {
p.processStates[key] = &types.ProcessState{
Name: key,
Status: types.ProcessStatePending,
SystemTime: "",
Health: types.ProcessHealthUnknown,
Restarts: 0,
ExitCode: 0,
Pid: 0,
}
if proc.Disabled {
p.processStates[key].Status = types.ProcessStateDisabled
}
}
}
func (p *ProjectRunner) initProcessLogs() {
p.processLogs = make(map[string]*pclog.ProcessLogBuffer)
for key := range p.project.Processes {
p.processLogs[key] = pclog.NewLogBuffer(p.project.LogLength)
}
}
func (p *ProjectRunner) GetProcessState(name string) *types.ProcessState {
if procState, ok := p.processStates[name]; ok {
proc := p.getRunningProcess(name)
if proc != nil {
proc.updateProcState()
} else {
procState.Pid = 0
procState.SystemTime = ""
procState.Health = types.ProcessHealthUnknown
procState.IsRunning = false
}
return procState
}
log.Error().Msgf("Error: process %s doesn't exist", name)
return nil
}
func (p *ProjectRunner) addRunningProcess(process *Process) {
p.mapMutex.Lock()
p.runningProcesses[process.getName()] = process
p.mapMutex.Unlock()
}
func (p *ProjectRunner) getRunningProcess(name string) *Process {
p.mapMutex.Lock()
defer p.mapMutex.Unlock()
if runningProc, ok := p.runningProcesses[name]; ok {
return runningProc
}
return nil
}
func (p *ProjectRunner) removeRunningProcess(name string) {
p.mapMutex.Lock()
delete(p.runningProcesses, name)
p.mapMutex.Unlock()
}
func (p *ProjectRunner) StartProcess(name string) error {
proc := p.getRunningProcess(name)
if proc != nil {
log.Error().Msgf("Process %s is already running", name)
return fmt.Errorf("process %s is already running", name)
}
if processConfig, ok := p.project.Processes[name]; ok {
processConfig.Name = name
p.runProcess(processConfig)
} else {
return fmt.Errorf("no such process: %s", name)
}
return nil
}
func (p *ProjectRunner) StopProcess(name string) error {
proc := p.getRunningProcess(name)
if proc == nil {
log.Error().Msgf("Process %s is not running", name)
return fmt.Errorf("process %s is not running", name)
}
_ = proc.shutDown()
return nil
}
func (p *ProjectRunner) RestartProcess(name string) error {
proc := p.getRunningProcess(name)
if proc != nil {
_ = proc.shutDown()
if proc.isRestartable() {
return nil
}
time.Sleep(proc.getBackoff())
}
if processConfig, ok := p.project.Processes[name]; ok {
processConfig.Name = name
p.runProcess(processConfig)
} else {
return fmt.Errorf("no such process: %s", name)
}
return nil
}
func (p *ProjectRunner) GetProcessInfo(name string) (*types.ProcessConfig, error) {
if processConfig, ok := p.project.Processes[name]; ok {
processConfig.Name = name
return &processConfig, nil
} else {
return nil, fmt.Errorf("no such process: %s", name)
}
}
func (p *ProjectRunner) ShutDownProject() {
p.mapMutex.Lock()
defer p.mapMutex.Unlock()
runProc := p.runningProcesses
for _, proc := range runProc {
proc.prepareForShutDown()
}
for _, proc := range runProc {
_ = proc.shutDown()
}
}
func (p *ProjectRunner) getProcessLog(name string) (*pclog.ProcessLogBuffer, error) {
if procLogs, ok := p.processLogs[name]; ok {
return procLogs, nil
}
log.Error().Msgf("Error: process %s doesn't exist", name)
return nil, fmt.Errorf("process %s doesn't exist", name)
}
func (p *ProjectRunner) GetProcessLog(name string, offsetFromEnd, limit int) ([]string, error) {
logs, err := p.getProcessLog(name)
if err != nil {
return nil, err
}
return logs.GetLogRange(offsetFromEnd, limit), nil
}
func (p *ProjectRunner) GetProcessLogLine(name string, lineIndex int) (string, error) {
logs, err := p.getProcessLog(name)
if err != nil {
return "", err
}
return logs.GetLogLine(lineIndex), nil
}
func (p *ProjectRunner) GetProcessLogLength(name string) int {
logs, err := p.getProcessLog(name)
if err != nil {
return 0
}
return logs.GetLogLength()
}
func (p *ProjectRunner) GetLogsAndSubscribe(name string, observer pclog.PcLogObserver) {
logs, err := p.getProcessLog(name)
if err != nil {
return
}
logs.GetLogsAndSubscribe(observer)
}
func (p *ProjectRunner) UnSubscribeLogger(name string) {
logs, err := p.getProcessLog(name)
if err != nil {
return
}
logs.UnSubscribe()
}
func (p *ProjectRunner) selectRunningProcesses(procList []string) error {
if len(procList) == 0 {
return nil
}
newProcMap := types.Processes{}
err := p.project.WithProcesses(procList, func(process types.ProcessConfig) error {
newProcMap[process.Name] = process
return nil
})
if err != nil {
log.Err(err).Msgf("Failed select processes")
return err
}
p.project.Processes = newProcMap
return nil
}
func (p *ProjectRunner) selectRunningProcessesNoDeps(procList []string) error {
if len(procList) == 0 {
return nil
}
newProcMap := types.Processes{}
for _, procName := range procList {
if conf, ok := p.project.Processes[procName]; ok {
conf.DependsOn = types.DependsOnConfig{}
newProcMap[procName] = conf
} else {
err := fmt.Errorf("no such process: %s", procName)
log.Err(err).Msgf("Failed select processes")
return err
}
}
p.project.Processes = newProcMap
return nil
}
func (p *ProjectRunner) GetLogLength() int {
return p.project.LogLength
}
func (p *ProjectRunner) GetDependenciesOrderNames() ([]string, error) {
return p.project.GetDependenciesOrderNames()
}
func (p *ProjectRunner) GetProject() *types.Project {
return p.project
}
func NewProjectRunner(project *types.Project, processesToRun []string, noDeps bool) (*ProjectRunner, error) {
runner := &ProjectRunner{
project: project,
}
var err error
if noDeps {
err = runner.selectRunningProcessesNoDeps(processesToRun)
} else {
err = runner.selectRunningProcesses(processesToRun)
}
if err != nil {
return nil, err
}
PROJ = runner
runner.init()
return runner, nil
}

View File

@ -1,6 +1,7 @@
package app
import (
"github.com/f1bonacc1/process-compose/src/types"
"reflect"
"testing"
)
@ -10,7 +11,7 @@ func TestProject_GetDependenciesOrderNames(t *testing.T) {
Version string
LogLevel string
LogLocation string
Processes map[string]ProcessConfig
Processes map[string]types.ProcessConfig
Environment []string
}
tests := []struct {
@ -22,22 +23,22 @@ func TestProject_GetDependenciesOrderNames(t *testing.T) {
{
name: "ShouldBe_4321",
fields: fields{
Processes: map[string]ProcessConfig{
Processes: map[string]types.ProcessConfig{
"Process1": {
Name: "Process1",
DependsOn: DependsOnConfig{
DependsOn: types.DependsOnConfig{
"Process2": {},
},
},
"Process2": {
Name: "Process2",
DependsOn: DependsOnConfig{
DependsOn: types.DependsOnConfig{
"Process3": {},
},
},
"Process3": {
Name: "Process3",
DependsOn: DependsOnConfig{
DependsOn: types.DependsOnConfig{
"Process4": {},
},
},
@ -52,16 +53,16 @@ func TestProject_GetDependenciesOrderNames(t *testing.T) {
{
name: "ShouldBe_Err",
fields: fields{
Processes: map[string]ProcessConfig{
Processes: map[string]types.ProcessConfig{
"Process1": {
Name: "Process1",
DependsOn: DependsOnConfig{
DependsOn: types.DependsOnConfig{
"Process2": {},
},
},
"Process2": {
Name: "Process2",
DependsOn: DependsOnConfig{
DependsOn: types.DependsOnConfig{
"Process4": {},
},
},
@ -73,10 +74,10 @@ func TestProject_GetDependenciesOrderNames(t *testing.T) {
{
name: "ShouldBe_1",
fields: fields{
Processes: map[string]ProcessConfig{
Processes: map[string]types.ProcessConfig{
"Process1": {
Name: "Process1",
DependsOn: DependsOnConfig{
DependsOn: types.DependsOnConfig{
"Process2": {},
},
},
@ -92,11 +93,11 @@ func TestProject_GetDependenciesOrderNames(t *testing.T) {
{
name: "ShouldBe_2",
fields: fields{
Processes: map[string]ProcessConfig{
Processes: map[string]types.ProcessConfig{
"Process1": {
Name: "Process1",
Disabled: true,
DependsOn: DependsOnConfig{
DependsOn: types.DependsOnConfig{
"Process2": {},
},
},
@ -111,7 +112,7 @@ func TestProject_GetDependenciesOrderNames(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Project{
p := &types.Project{
Version: tt.fields.Version,
LogLocation: tt.fields.LogLocation,
Processes: tt.fields.Processes,

View File

@ -1,6 +1,7 @@
package app
import (
"github.com/f1bonacc1/process-compose/src/loader"
"os"
"path/filepath"
"reflect"
@ -31,12 +32,19 @@ func TestSystem_TestFixtures(t *testing.T) {
}
t.Run(fixture, func(t *testing.T) {
project, err := NewProject(fixture, []string{}, false)
project, err := loader.Load(&loader.LoaderOptions{
FileNames: []string{fixture},
})
if err != nil {
t.Errorf(err.Error())
return
}
project.Run()
runner, err := NewProjectRunner(project, []string{}, false)
if err != nil {
t.Errorf(err.Error())
return
}
runner.Run()
})
}
}
@ -44,20 +52,27 @@ func TestSystem_TestFixtures(t *testing.T) {
func TestSystem_TestComposeWithLog(t *testing.T) {
fixture := filepath.Join("..", "..", "fixtures", "process-compose-with-log.yaml")
t.Run(fixture, func(t *testing.T) {
project, err := NewProject(fixture, []string{}, false)
project, err := loader.Load(&loader.LoaderOptions{
FileNames: []string{fixture},
})
if err != nil {
t.Errorf(err.Error())
return
}
project.Run()
if _, err := os.Stat(project.LogLocation); err != nil {
t.Errorf("log file %s not found", project.LogLocation)
runner, err := NewProjectRunner(project, []string{}, false)
if err != nil {
t.Errorf(err.Error())
return
}
if err := os.Remove(project.LogLocation); err != nil {
t.Errorf("failed to delete the log file %s, %s", project.LogLocation, err.Error())
runner.Run()
if _, err := os.Stat(runner.project.LogLocation); err != nil {
t.Errorf("log file %s not found", runner.project.LogLocation)
}
if err := os.Remove(runner.project.LogLocation); err != nil {
t.Errorf("failed to delete the log file %s, %s", runner.project.LogLocation, err.Error())
}
proc6log := project.Processes["process6"].LogLocation
proc6log := runner.project.Processes["process6"].LogLocation
if _, err := os.Stat(proc6log); err != nil {
t.Errorf("log file %s not found", proc6log)
}
@ -70,12 +85,19 @@ func TestSystem_TestComposeWithLog(t *testing.T) {
func TestSystem_TestComposeChain(t *testing.T) {
fixture := filepath.Join("..", "..", "fixtures", "process-compose-chain.yaml")
t.Run(fixture, func(t *testing.T) {
project, err := NewProject(fixture, []string{}, false)
project, err := loader.Load(&loader.LoaderOptions{
FileNames: []string{fixture},
})
if err != nil {
t.Errorf(err.Error())
return
}
names, err := project.GetDependenciesOrderNames()
runner, err := NewProjectRunner(project, []string{}, false)
if err != nil {
t.Errorf(err.Error())
return
}
names, err := runner.GetDependenciesOrderNames()
if err != nil {
t.Errorf("GetDependenciesOrderNames() error = %v", err)
return
@ -99,56 +121,22 @@ func TestSystem_TestComposeChain(t *testing.T) {
func TestSystem_TestComposeChainExit(t *testing.T) {
fixture := filepath.Join("..", "..", "fixtures", "process-compose-chain-exit.yaml")
t.Run(fixture, func(t *testing.T) {
project, err := NewProject(fixture, []string{}, false)
project, err := loader.Load(&loader.LoaderOptions{
FileNames: []string{fixture},
})
if err != nil {
t.Errorf(err.Error())
return
}
exitCode := project.Run()
runner, err := NewProjectRunner(project, []string{}, false)
if err != nil {
t.Errorf(err.Error())
return
}
exitCode := runner.Run()
want := 42
if want != exitCode {
t.Errorf("Project.Run() = %v, want %v", exitCode, want)
}
})
}
func Test_autoDiscoverComposeFile(t *testing.T) {
type args struct {
pwd string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "Should not find",
args: args{
pwd: "../../fixtures",
},
want: "",
wantErr: true,
},
{
name: "Should find process-compose.yaml",
args: args{
pwd: "../../",
},
want: filepath.Join("..", "..", "process-compose.yaml"),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := AutoDiscoverComposeFile(tt.args.pwd)
if (err != nil) != tt.wantErr {
t.Errorf("autoDiscoverComposeFile() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("autoDiscoverComposeFile() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -3,7 +3,7 @@ package client
import (
"encoding/json"
"fmt"
"github.com/f1bonacc1/process-compose/src/app"
"github.com/f1bonacc1/process-compose/src/types"
"net/http"
)
@ -15,7 +15,7 @@ func GetProcessesName(address string, port int) ([]string, error) {
}
defer resp.Body.Close()
//Create a variable of the same type as our model
var sResp app.ProcessStates
var sResp types.ProcessStates
//Decode the data
if err := json.NewDecoder(resp.Body).Decode(&sResp); err != nil {

View File

@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"github.com/f1bonacc1/process-compose/src/app"
"github.com/f1bonacc1/process-compose/src/loader"
"github.com/f1bonacc1/process-compose/src/tui"
"github.com/rs/zerolog/log"
"os"
@ -10,30 +11,23 @@ import (
"syscall"
)
func runProject(isDefConfigPath bool, process []string, noDeps bool) {
if isDefConfigPath {
pwd, err := os.Getwd()
if err != nil {
log.Fatal().Msg(err.Error())
}
file, err := app.AutoDiscoverComposeFile(pwd)
if err != nil {
fmt.Println(err)
log.Fatal().Msg(err.Error())
}
fileName = file
func runProject(process []string, noDeps bool) {
project, err := loader.Load(opts)
if err != nil {
fmt.Println(err)
log.Fatal().Msg(err.Error())
}
project, err := app.NewProject(fileName, process, noDeps)
runner, err := app.NewProjectRunner(project, process, noDeps)
if err != nil {
fmt.Println(err)
log.Fatal().Msg(err.Error())
}
exitCode := 0
if isTui {
exitCode = runTui(project)
exitCode = runTui(runner)
} else {
exitCode = runHeadless(project)
exitCode = runHeadless(runner)
}
log.Info().Msg("Thank you for using process-compose")
@ -51,7 +45,7 @@ func setSignal(signalHandler func()) {
}()
}
func runHeadless(project *app.Project) int {
func runHeadless(project *app.ProjectRunner) int {
setSignal(func() {
project.ShutDownProject()
})
@ -59,12 +53,12 @@ func runHeadless(project *app.Project) int {
return exitCode
}
func runTui(project *app.Project) int {
func runTui(project *app.ProjectRunner) int {
setSignal(func() {
tui.Stop()
})
defer quiet()()
go tui.SetupTui(project.LogLength)
go tui.SetupTui()
exitCode := project.Run()
tui.Stop()
return exitCode

View File

@ -2,24 +2,23 @@ package cmd
import (
"github.com/f1bonacc1/process-compose/src/api"
"github.com/f1bonacc1/process-compose/src/app"
"github.com/f1bonacc1/process-compose/src/loader"
"github.com/spf13/cobra"
"os"
)
var (
fileName string
port int
isTui bool
port int
isTui bool
opts *loader.LoaderOptions
// rootCmd represents the base command when called without any subcommands
rootCmd = &cobra.Command{
Use: "process-compose",
Short: "Processes scheduler and orchestrator",
Run: func(cmd *cobra.Command, args []string) {
isDefConfigPath := !cmd.Flags().Changed("config")
api.StartHttpServer(!isTui, port)
runProject(isDefConfigPath, []string{}, false)
runProject([]string{}, false)
},
}
)
@ -34,8 +33,10 @@ func Execute() {
}
func init() {
rootCmd.Flags().StringVarP(&fileName, "config", "f", app.DefaultFileNames[0], "path to config file to load")
opts = &loader.LoaderOptions{
FileNames: []string{},
}
rootCmd.Flags().BoolVarP(&isTui, "tui", "t", true, "disable tui (-t=false)")
rootCmd.PersistentFlags().IntVarP(&port, "port", "p", 8080, "port number")
rootCmd.PersistentFlags().StringArrayVarP(&opts.FileNames, "config", "f", []string{}, "path to config files to load")
}

View File

@ -2,7 +2,6 @@ package cmd
import (
"github.com/f1bonacc1/process-compose/src/api"
"github.com/f1bonacc1/process-compose/src/app"
"github.com/spf13/cobra"
)
@ -18,16 +17,14 @@ var upCmd = &cobra.Command{
If one or more process names are passed as arguments,
will start them and their dependencies only`,
Run: func(cmd *cobra.Command, args []string) {
isDefConfigPath := !cmd.Flags().Changed("config")
api.StartHttpServer(!isTui, port)
runProject(isDefConfigPath, args, noDeps)
runProject(args, noDeps)
},
}
func init() {
rootCmd.AddCommand(upCmd)
upCmd.Flags().StringVarP(&fileName, "config", "f", app.DefaultFileNames[0], "path to config file to load")
upCmd.Flags().BoolVarP(&isTui, "tui", "t", true, "disable tui (-t=false)")
upCmd.Flags().BoolVarP(&noDeps, "no-deps", "", false, "don't start dependent processes")
}

115
src/loader/loader.go Normal file
View File

@ -0,0 +1,115 @@
package loader
import (
"errors"
"fmt"
"github.com/f1bonacc1/process-compose/src/types"
"github.com/joho/godotenv"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
"os"
"path/filepath"
"strings"
)
const (
defaultLogLength = 1000
)
func Load(opts *LoaderOptions) (*types.Project, error) {
err := autoDiscoverComposeFile(opts)
if err != nil {
return nil, err
}
for _, file := range opts.FileNames {
p := mustLoadProjectFromFile(file)
opts.projects = append(opts.projects, p)
}
return merge(opts)
}
func mustLoadProjectFromFile(inputFile string) *types.Project {
yamlFile, err := os.ReadFile(inputFile)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
log.Error().Msgf("File %s doesn't exist", inputFile)
}
log.Fatal().Msg(err.Error())
}
// .env is optional we don't care if it errors
_ = godotenv.Load()
yamlFile = []byte(os.ExpandEnv(string(yamlFile)))
project := &types.Project{
LogLength: defaultLogLength,
}
err = yaml.Unmarshal(yamlFile, project)
if err != nil {
fmt.Printf("Failed to parse %s - %s\n", inputFile, err.Error())
log.Fatal().Msgf("Failed to parse %s - %s", inputFile, err.Error())
}
project.Validate()
log.Info().Msgf("Loaded project from %s", inputFile)
return project
}
func findFiles(names []string, pwd string) []string {
candidates := []string{}
for _, n := range names {
f := filepath.Join(pwd, n)
if _, err := os.Stat(f); err == nil {
candidates = append(candidates, f)
}
}
return candidates
}
// DefaultFileNames defines the Compose file names for auto-discovery (in order of preference)
var DefaultFileNames = []string{
"compose.yml",
"compose.yaml",
"process-compose.yml",
"process-compose.yaml",
}
// DefaultOverrideFileNames defines the Compose override file names for auto-discovery (in order of preference)
var DefaultOverrideFileNames = []string{
"compose.override.yml",
"compose.override.yaml",
"process-compose.override.yml",
"process-compose.override.yaml",
}
func autoDiscoverComposeFile(opts *LoaderOptions) error {
if len(opts.FileNames) > 0 {
return nil
}
pwd, err := opts.getWorkingDir()
if err != nil {
return err
}
candidates := findFiles(DefaultFileNames, pwd)
if len(candidates) > 0 {
if len(candidates) > 1 {
log.Warn().Msgf("Found multiple config files with supported names: %s", strings.Join(candidates, ", "))
log.Warn().Msgf("Using %s", candidates[0])
}
opts.FileNames = append(opts.FileNames, candidates[0])
overrides := findFiles(DefaultOverrideFileNames, pwd)
if len(overrides) > 0 {
if len(overrides) > 1 {
log.Warn().Msgf("Found multiple override files with supported names: %s", strings.Join(overrides, ", "))
log.Warn().Msgf("Using %s", overrides[0])
}
opts.FileNames = append(opts.FileNames, overrides[0])
}
return nil
}
return fmt.Errorf("no config files found in %s", pwd)
}

View File

@ -0,0 +1,29 @@
package loader
import (
"github.com/f1bonacc1/process-compose/src/types"
"os"
"path/filepath"
)
type LoaderOptions struct {
workingDir string
FileNames []string
projects []*types.Project
}
func (o LoaderOptions) getWorkingDir() (string, error) {
if o.workingDir != "" {
return o.workingDir, nil
}
for _, path := range o.FileNames {
if path != "-" {
absPath, err := filepath.Abs(path)
if err != nil {
return "", err
}
return filepath.Dir(absPath), nil
}
}
return os.Getwd()
}

View File

@ -0,0 +1,50 @@
package loader
import (
"github.com/f1bonacc1/process-compose/src/types"
"testing"
)
func TestLoaderOptions_getWorkingDir(t *testing.T) {
type fields struct {
workingDir string
FileNames []string
projects []*types.Project
}
tests := []struct {
name string
fields fields
want string
wantErr bool
}{
{
name: "Empty Working Dir",
fields: fields{
workingDir: "",
FileNames: []string{
"/home/user/dir/process-compose.yaml",
},
projects: nil,
},
want: "/home/user/dir",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := LoaderOptions{
workingDir: tt.fields.workingDir,
FileNames: tt.fields.FileNames,
projects: tt.fields.projects,
}
got, err := o.getWorkingDir()
if (err != nil) != tt.wantErr {
t.Errorf("getWorkingDir() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("getWorkingDir() got = %v, want %v", got, tt.want)
}
})
}
}

63
src/loader/loader_test.go Normal file
View File

@ -0,0 +1,63 @@
package loader
import (
"testing"
)
func Test_autoDiscoverComposeFile(t *testing.T) {
type args struct {
opts *LoaderOptions
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Should not find",
args: args{
opts: &LoaderOptions{
workingDir: "../../fixtures",
FileNames: nil,
projects: nil,
},
},
wantErr: true,
},
{
name: "Should find process-compose.yaml",
args: args{
opts: &LoaderOptions{
workingDir: "../../",
FileNames: nil,
projects: nil,
},
},
wantErr: false,
},
{
name: "Should find process-compose.yaml no CWD",
args: args{
opts: &LoaderOptions{
workingDir: "",
FileNames: []string{"../../process-compose.yaml"},
projects: nil,
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := autoDiscoverComposeFile(tt.args.opts); (err != nil) != tt.wantErr {
t.Errorf("autoDiscoverComposeFile() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr {
filesNum := len(tt.args.opts.FileNames)
if filesNum == 0 {
t.Errorf("autoDiscoverComposeFile() filesNum = %v, want Files %v", filesNum, 1)
}
}
})
}
}

163
src/loader/merger.go Normal file
View File

@ -0,0 +1,163 @@
package loader
import (
"fmt"
"github.com/f1bonacc1/process-compose/src/types"
"github.com/imdario/mergo"
"github.com/pkg/errors"
"reflect"
"sort"
"strings"
)
type specials struct {
m map[reflect.Type]func(dst, src reflect.Value) error
}
var processSpecials = &specials{
m: map[reflect.Type]func(dst, src reflect.Value) error{
reflect.TypeOf(types.Environment{}): mergeSlice(toEnvVarMap, toEnvVarSlice),
},
}
var projectSpecials = &specials{
m: map[reflect.Type]func(dst, src reflect.Value) error{
reflect.TypeOf(types.Environment{}): mergeSlice(toEnvVarMap, toEnvVarSlice),
reflect.TypeOf(types.Processes{}): specialProcessesMerge,
},
}
func (s *specials) Transformer(t reflect.Type) func(dst, src reflect.Value) error {
if fn, ok := s.m[t]; ok {
return fn
}
return nil
}
type toMapFn func(s interface{}) (map[interface{}]interface{}, error)
type writeValueFromMapFn func(reflect.Value, map[interface{}]interface{}) error
func mergeSlice(toMap toMapFn, writeValue writeValueFromMapFn) func(dst, src reflect.Value) error {
return func(dst, src reflect.Value) error {
dstMap, err := sliceToMap(toMap, dst)
if err != nil {
return err
}
srcMap, err := sliceToMap(toMap, src)
if err != nil {
return err
}
if err := mergo.Map(&dstMap, srcMap, mergo.WithOverride); err != nil {
return err
}
return writeValue(dst, dstMap)
}
}
func sliceToMap(toMap toMapFn, v reflect.Value) (map[interface{}]interface{}, error) {
// check if valid
if !v.IsValid() {
return nil, errors.Errorf("invalid value : %+v", v)
}
return toMap(v.Interface())
}
func toEnvVarMap(s interface{}) (map[interface{}]interface{}, error) {
envVars, ok := s.(types.Environment)
if !ok {
return nil, errors.Errorf("not an Environment slice: %v", s)
}
m := map[interface{}]interface{}{}
for _, v := range envVars {
kv := strings.Split(v, "=")
if len(kv) == 2 {
m[kv[0]] = kv[1]
}
}
return m, nil
}
func toEnvVarSlice(dst reflect.Value, m map[interface{}]interface{}) error {
var s types.Environment
for k, v := range m {
kv := fmt.Sprintf("%s=%s", k.(string), v.(string))
s = append(s, kv)
}
sort.Strings(s)
dst.Set(reflect.ValueOf(s))
return nil
}
func merge(opts *LoaderOptions) (*types.Project, error) {
base := opts.projects[0]
if len(opts.projects) == 1 {
return base, nil
}
for i, override := range opts.projects[1:] {
if err := mergeProjects(base, override); err != nil {
return base, fmt.Errorf("cannot merge projects from %s - %v", opts.FileNames[i], err)
}
}
return base, nil
}
func specialProcessesMerge(dst, src reflect.Value) error {
if !dst.IsValid() {
return errors.Errorf("invalid value: %+v", dst)
}
if !src.IsValid() {
return errors.Errorf("invalid value: %+v", src)
}
var (
dstProc types.Processes
srcProc types.Processes
ok bool
)
if dstProc, ok = dst.Interface().(types.Processes); !ok {
return errors.Errorf("invalid type: %+v", dst)
}
if srcProc, ok = src.Interface().(types.Processes); !ok {
return errors.Errorf("invalid type: %+v", src)
}
merged, err := mergeProcesses(dstProc, srcProc)
dst.Set(reflect.ValueOf(merged))
return err
}
func mergeProcesses(base, override types.Processes) (types.Processes, error) {
for name, overrideProcess := range override {
overrideProcess := overrideProcess
if baseProcess, ok := base[name]; ok {
merged, err := mergeProcess(&baseProcess, &overrideProcess)
if err != nil {
return nil, fmt.Errorf("cannot merge process %s - %v", name, err)
}
base[name] = *merged
continue
}
base[name] = overrideProcess
}
return base, nil
}
func mergeProcess(base, override *types.ProcessConfig) (*types.ProcessConfig, error) {
if err := mergo.Merge(base, override,
mergo.WithAppendSlice,
mergo.WithOverride,
mergo.WithTransformers(processSpecials)); err != nil {
return nil, err
}
return base, nil
}
func mergeProjects(base, override *types.Project) error {
if err := mergo.Merge(base, override,
mergo.WithAppendSlice,
mergo.WithOverride,
mergo.WithTransformers(projectSpecials)); err != nil {
return err
}
return nil
}

418
src/loader/merger_test.go Normal file
View File

@ -0,0 +1,418 @@
package loader
import (
"github.com/f1bonacc1/process-compose/src/health"
"github.com/f1bonacc1/process-compose/src/types"
"reflect"
"testing"
)
func getBaseProcess() *types.ProcessConfig {
return &types.ProcessConfig{
Name: "proc1",
Disabled: false,
IsDaemon: false,
Command: "command",
LogLocation: "",
Environment: types.Environment{
"k1=v1",
"k2=v2",
},
RestartPolicy: types.RestartPolicyConfig{
Restart: "no",
BackoffSeconds: 1,
MaxRestarts: 1,
},
DependsOn: types.DependsOnConfig{
"proc1": {
Condition: "process_completed",
},
"proc3": {
Condition: "process_completed_successfully",
},
},
LivenessProbe: nil,
ReadinessProbe: &health.Probe{
HttpGet: &health.HttpProbe{
Host: "127.0.0.1",
Path: "/is",
Scheme: "http",
Port: 80,
},
InitialDelay: 5,
PeriodSeconds: 4,
TimeoutSeconds: 3,
SuccessThreshold: 2,
FailureThreshold: 1,
},
ShutDownParams: types.ShutDownParams{
ShutDownCommand: "command",
ShutDownTimeout: 3,
Signal: 1,
},
DisableAnsiColors: false,
WorkingDir: "working/dir",
Extensions: nil,
}
}
func getOverrideProcess() *types.ProcessConfig {
return &types.ProcessConfig{
Name: "proc1",
Disabled: false,
IsDaemon: false,
Command: "override command",
LogLocation: "",
Environment: types.Environment{
"k0=v0",
"k1=override",
"k3=v3",
"k4=v4",
},
RestartPolicy: types.RestartPolicyConfig{
Restart: "always",
BackoffSeconds: 2,
MaxRestarts: 2,
},
DependsOn: types.DependsOnConfig{
"proc1": {
Condition: "process_completed_successfully",
},
"proc2": {
Condition: "process_completed_successfully",
},
},
LivenessProbe: &health.Probe{
HttpGet: &health.HttpProbe{
Host: "google.com",
Path: "/isAlive",
Scheme: "https",
Port: 443,
},
InitialDelay: 1,
PeriodSeconds: 2,
TimeoutSeconds: 3,
SuccessThreshold: 4,
FailureThreshold: 5,
},
ReadinessProbe: &health.Probe{
HttpGet: &health.HttpProbe{
Host: "google.com",
Path: "/isAlive",
Scheme: "https",
Port: 443,
},
InitialDelay: 1,
PeriodSeconds: 2,
TimeoutSeconds: 3,
SuccessThreshold: 4,
FailureThreshold: 5,
},
ShutDownParams: types.ShutDownParams{
ShutDownCommand: "override command",
ShutDownTimeout: 1,
Signal: 2,
},
DisableAnsiColors: true,
//WorkingDir: "",
Extensions: nil,
}
}
func getMergedProcess() *types.ProcessConfig {
return &types.ProcessConfig{
Name: "proc1",
Disabled: false,
IsDaemon: false,
Command: "override command",
LogLocation: "",
Environment: types.Environment{
"k0=v0",
"k1=override",
"k2=v2",
"k3=v3",
"k4=v4",
},
RestartPolicy: types.RestartPolicyConfig{
Restart: "always",
BackoffSeconds: 2,
MaxRestarts: 2,
},
DependsOn: types.DependsOnConfig{
"proc1": {
Condition: "process_completed_successfully",
},
"proc2": {
Condition: "process_completed_successfully",
},
"proc3": {
Condition: "process_completed_successfully",
},
},
LivenessProbe: &health.Probe{
HttpGet: &health.HttpProbe{
Host: "google.com",
Path: "/isAlive",
Scheme: "https",
Port: 443,
},
InitialDelay: 1,
PeriodSeconds: 2,
TimeoutSeconds: 3,
SuccessThreshold: 4,
FailureThreshold: 5,
},
ReadinessProbe: &health.Probe{
HttpGet: &health.HttpProbe{
Host: "google.com",
Path: "/isAlive",
Scheme: "https",
Port: 443,
},
InitialDelay: 1,
PeriodSeconds: 2,
TimeoutSeconds: 3,
SuccessThreshold: 4,
FailureThreshold: 5,
},
ShutDownParams: types.ShutDownParams{
ShutDownCommand: "override command",
ShutDownTimeout: 1,
Signal: 2,
},
DisableAnsiColors: true,
WorkingDir: "working/dir",
Extensions: nil,
}
}
func Test_mergeProcess(t *testing.T) {
type args struct {
baseProcess *types.ProcessConfig
overrideProcess *types.ProcessConfig
}
tests := []struct {
name string
args args
want *types.ProcessConfig
wantErr bool
}{
{
name: "command",
args: args{
baseProcess: getBaseProcess(),
overrideProcess: getOverrideProcess(),
},
want: getMergedProcess(),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := mergeProcess(tt.args.baseProcess, tt.args.overrideProcess)
if (err != nil) != tt.wantErr {
t.Errorf("mergeProcess() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mergeProcess() got = %v, want %v", got, tt.want)
}
})
}
}
func Test_merge(t *testing.T) {
type args struct {
opts *LoaderOptions
}
tests := []struct {
name string
args args
want *types.Project
wantErr bool
}{
{
name: "No Processes",
args: args{
opts: &LoaderOptions{
workingDir: "",
FileNames: nil,
projects: []*types.Project{
{
Version: "0.5",
LogLocation: "loc1",
LogLevel: "Debug",
LogLength: 100,
Processes: nil,
Environment: types.Environment{
"k1=v1",
"k2=v2",
},
ShellConfig: nil,
},
{
Version: "0.6",
LogLocation: "loc2",
LogLevel: "Info",
LogLength: 200,
Processes: nil,
Environment: types.Environment{
"k0=v0",
"k1=override",
"k3=v3",
"k4=v4",
},
ShellConfig: nil,
},
},
},
},
want: &types.Project{
Version: "0.6",
LogLocation: "loc2",
LogLevel: "Info",
LogLength: 200,
Processes: nil,
Environment: types.Environment{
"k0=v0",
"k1=override",
"k2=v2",
"k3=v3",
"k4=v4",
},
ShellConfig: nil,
},
wantErr: false,
},
{
name: "With Single Process",
args: args{
opts: &LoaderOptions{
workingDir: "",
FileNames: nil,
projects: []*types.Project{
{
Version: "",
LogLocation: "",
LogLevel: "",
LogLength: 0,
Processes: types.Processes{
"proc1": *getBaseProcess(),
},
Environment: nil,
ShellConfig: nil,
},
{
Version: "",
LogLocation: "",
LogLevel: "",
LogLength: 0,
Processes: types.Processes{
"proc1": *getOverrideProcess(),
},
Environment: nil,
ShellConfig: nil,
},
},
},
},
want: &types.Project{
Version: "",
LogLocation: "",
LogLevel: "",
LogLength: 0,
Processes: types.Processes{
"proc1": *getMergedProcess(),
},
Environment: nil,
ShellConfig: nil,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := merge(tt.args.opts)
if (err != nil) != tt.wantErr {
t.Errorf("merge() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("merge() got = %+v, want %+v", got, tt.want)
}
})
}
}
func Test_mergeProcesses(t *testing.T) {
type args struct {
base types.Processes
override types.Processes
}
tests := []struct {
name string
args args
want types.Processes
wantErr bool
}{
{
name: "Single Process",
args: args{
base: types.Processes{
"proc1": *getBaseProcess(),
},
override: types.Processes{
"proc1": *getOverrideProcess(),
},
},
want: types.Processes{
"proc1": *getMergedProcess(),
},
wantErr: false,
},
{
name: "No Override",
args: args{
base: types.Processes{
"proc1": *getBaseProcess(),
},
override: types.Processes{},
},
want: types.Processes{
"proc1": *getBaseProcess(),
},
wantErr: false,
},
{
name: "Multiple Processes",
args: args{
base: types.Processes{
"proc1": *getBaseProcess(),
"proc2": *getBaseProcess(),
},
override: types.Processes{
"proc1": *getOverrideProcess(),
"proc3": *getBaseProcess(),
},
},
want: types.Processes{
"proc1": *getMergedProcess(),
"proc2": *getBaseProcess(),
"proc3": *getBaseProcess(),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := mergeProcesses(tt.args.base, tt.args.override)
if (err != nil) != tt.wantErr {
t.Errorf("mergeProcesses() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mergeProcesses() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -3,6 +3,7 @@ package tui
import (
"github.com/f1bonacc1/glippy"
"github.com/f1bonacc1/process-compose/src/app"
"github.com/f1bonacc1/process-compose/src/types"
"github.com/gdamore/tcell/v2"
"time"
)
@ -46,7 +47,7 @@ func (pv *pcView) stopFollowLog() {
func (pv *pcView) followLog(name string) {
pv.loggedProc = name
pv.logsText.Clear()
_ = app.PROJ.WithProcesses([]string{name}, func(process app.ProcessConfig) error {
_ = app.PROJ.GetProject().WithProcesses([]string{name}, func(process types.ProcessConfig) error {
pv.logsText.useAnsi = !process.DisableAnsiColors
return nil
})

View File

@ -1,13 +1,13 @@
package tui
import (
"github.com/f1bonacc1/process-compose/src/app"
"github.com/f1bonacc1/process-compose/src/types"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"strings"
)
func (pv *pcView) createProcInfoForm(info *app.ProcessConfig) *tview.Form {
func (pv *pcView) createProcInfoForm(info *types.ProcessConfig) *tview.Form {
f := tview.NewForm()
f.SetCancelFunc(func() {
pv.pages.RemovePage(PageDialog)

View File

@ -45,13 +45,13 @@ type pcView struct {
logsTextArea *tview.TextArea
}
func newPcView(logLength int) *pcView {
func newPcView() *pcView {
//_ = pv.shortcuts.loadFromFile("short-cuts-new.yaml")
pv := &pcView{
appView: tview.NewApplication(),
logsText: NewLogView(logLength),
logsText: NewLogView(app.PROJ.GetLogLength()),
statusText: tview.NewTextView().SetDynamicColors(true),
procNames: app.PROJ.GetLexicographicProcessNames(),
procNames: app.PROJ.GetProject().GetLexicographicProcessNames(),
logFollow: true,
fullScrState: LogProcHalf,
helpText: tview.NewTextView().SetDynamicColors(true),
@ -231,9 +231,9 @@ func (pv *pcView) showUpdateAvailable(version string) {
})
}
func SetupTui(logLength int) {
func SetupTui() {
pv := newPcView(logLength)
pv := newPcView()
go pv.updateTable()
go pv.updateLogs()

View File

@ -1,4 +1,4 @@
package app
package types
import (
"github.com/rs/zerolog/log"

View File

@ -1,39 +1,16 @@
package app
package types
import (
"github.com/f1bonacc1/process-compose/src/command"
"sync"
"github.com/f1bonacc1/process-compose/src/health"
"github.com/f1bonacc1/process-compose/src/pclog"
)
type Project struct {
Version string `yaml:"version"`
LogLocation string `yaml:"log_location,omitempty"`
LogLevel string `yaml:"log_level,omitempty"`
LogLength int `yaml:"log_length,omitempty"`
Processes Processes `yaml:"processes"`
Environment []string `yaml:"environment,omitempty"`
ShellConfig *command.ShellConfig `yaml:"shell,omitempty"`
runningProcesses map[string]*Process
processStates map[string]*ProcessState
processLogs map[string]*pclog.ProcessLogBuffer
mapMutex sync.Mutex
logger pclog.PcLogger
wg sync.WaitGroup
exitCode int
}
import "github.com/f1bonacc1/process-compose/src/health"
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"`
LogLocation string `yaml:"log_location,omitempty"`
Environment []string `yaml:"environment,omitempty"`
Environment Environment `yaml:"environment,omitempty"`
RestartPolicy RestartPolicyConfig `yaml:"availability,omitempty"`
DependsOn DependsOnConfig `yaml:"depends_on,omitempty"`
LivenessProbe *health.Probe `yaml:"liveness_probe,omitempty"`
@ -44,6 +21,17 @@ type ProcessConfig struct {
Extensions map[string]interface{} `yaml:",inline"`
}
func (p ProcessConfig) GetDependencies() []string {
dependencies := make([]string, len(p.DependsOn))
i := 0
for k := range p.DependsOn {
dependencies[i] = k
i++
}
return dependencies
}
type ProcessState struct {
Name string `json:"name"`
Status string `json:"status"`
@ -59,17 +47,6 @@ type ProcessStates struct {
States []ProcessState `json:"data"`
}
func (p ProcessConfig) GetDependencies() []string {
dependencies := make([]string, len(p.DependsOn))
i := 0
for k := range p.DependsOn {
dependencies[i] = k
i++
}
return dependencies
}
const (
RestartPolicyAlways = "always"
RestartPolicyOnFailureDeprecated = "on-failure"

149
src/types/project.go Normal file
View File

@ -0,0 +1,149 @@
package types
import (
"fmt"
"github.com/f1bonacc1/process-compose/src/command"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"sort"
"strings"
)
type Project struct {
Version string `yaml:"version"`
LogLocation string `yaml:"log_location,omitempty"`
LogLevel string `yaml:"log_level,omitempty"`
LogLength int `yaml:"log_length,omitempty"`
Processes Processes `yaml:"processes"`
Environment Environment `yaml:"environment,omitempty"`
ShellConfig *command.ShellConfig `yaml:"shell,omitempty"`
}
func (p *Project) Validate() {
p.validateLogLevel()
p.setConfigDefaults()
p.deprecationCheck()
p.validateProcessConfig()
}
func (p *Project) validateLogLevel() {
if p.LogLevel != "" {
lvl, err := zerolog.ParseLevel(p.LogLevel)
if err != nil {
log.Warn().Msgf("Unknown log level %s defaulting to %s",
p.LogLevel, zerolog.GlobalLevel().String())
} else {
zerolog.SetGlobalLevel(lvl)
}
}
}
func (p *Project) setConfigDefaults() {
if p.ShellConfig == nil {
p.ShellConfig = command.DefaultShellConfig()
}
log.Info().Msgf("Global shell command: %s %s", p.ShellConfig.ShellCommand, p.ShellConfig.ShellArgument)
command.ValidateShellConfig(*p.ShellConfig)
}
func (p *Project) deprecationCheck() {
for key, proc := range p.Processes {
if proc.RestartPolicy.Restart == RestartPolicyOnFailureDeprecated {
deprecationHandler("2022-10-30", key, RestartPolicyOnFailureDeprecated, RestartPolicyOnFailure, "restart policy")
}
}
}
func (p *Project) validateProcessConfig() {
for key, proc := range p.Processes {
if len(proc.Extensions) == 0 {
continue
}
for extKey := range proc.Extensions {
if strings.HasPrefix(extKey, "x-") {
continue
}
log.Error().Msgf("Unknown key %s found in process %s", extKey, key)
}
}
}
type ProcessFunc func(process ProcessConfig) error
// WithProcesses run ProcesseFunc on each Process and dependencies in dependency order
func (p *Project) WithProcesses(names []string, fn ProcessFunc) error {
return p.withProcesses(names, fn, map[string]bool{})
}
func (p *Project) GetDependenciesOrderNames() ([]string, error) {
order := []string{}
err := p.WithProcesses([]string{}, func(process ProcessConfig) error {
order = append(order, process.Name)
return nil
})
return order, err
}
func (p *Project) GetLexicographicProcessNames() []string {
names := []string{}
for name := range p.Processes {
names = append(names, name)
}
sort.Strings(names)
return names
}
func (p *Project) getProcesses(names ...string) ([]ProcessConfig, error) {
processes := []ProcessConfig{}
if len(names) == 0 {
for name, proc := range p.Processes {
if proc.Disabled {
continue
}
proc.Name = name
processes = append(processes, proc)
}
return processes, nil
}
for _, name := range names {
if proc, ok := p.Processes[name]; ok {
if proc.Disabled {
continue
}
proc.Name = name
processes = append(processes, proc)
} else {
return processes, fmt.Errorf("no such process: %s", name)
}
}
return processes, nil
}
func (p *Project) withProcesses(names []string, fn ProcessFunc, done map[string]bool) error {
processes, err := p.getProcesses(names...)
if err != nil {
return err
}
for _, process := range processes {
if done[process.Name] {
continue
}
done[process.Name] = true
dependencies := process.GetDependencies()
if len(dependencies) > 0 {
err := p.withProcesses(dependencies, fn, done)
if err != nil {
return err
}
}
if err := fn(process); err != nil {
return err
}
}
return nil
}

View File

@ -21,4 +21,4 @@ do
fi
done
exit $EXIT_CODE
exit "$EXIT_CODE"