mirror of
https://github.com/F1bonacc1/process-compose.git
synced 2024-09-11 11:35:26 +03:00
Merge 2 or more configuration files
This commit is contained in:
parent
a1515562b7
commit
e064219f92
2
Makefile
2
Makefile
@ -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
2
go.mod
@ -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
4
go.sum
@ -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=
|
||||
|
132
process-compose.override.yaml
Normal file
132
process-compose.override.yaml
Normal 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"
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
364
src/app/project_runner.go
Normal 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
|
||||
}
|
@ -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,
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
func runProject(process []string, noDeps bool) {
|
||||
project, err := loader.Load(opts)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
fileName = file
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
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")
|
||||
}
|
||||
|
@ -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
115
src/loader/loader.go
Normal 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)
|
||||
}
|
29
src/loader/loader_options.go
Normal file
29
src/loader/loader_options.go
Normal 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()
|
||||
}
|
50
src/loader/loader_options_test.go
Normal file
50
src/loader/loader_options_test.go
Normal 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
63
src/loader/loader_test.go
Normal 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
163
src/loader/merger.go
Normal 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
418
src/loader/merger_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
})
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -1,4 +1,4 @@
|
||||
package app
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
@ -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
149
src/types/project.go
Normal 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
|
||||
}
|
@ -21,4 +21,4 @@ do
|
||||
fi
|
||||
|
||||
done
|
||||
exit $EXIT_CODE
|
||||
exit "$EXIT_CODE"
|
||||
|
Loading…
Reference in New Issue
Block a user