diff --git a/flake.nix b/flake.nix index 5d17b81..f226077 100644 --- a/flake.nix +++ b/flake.nix @@ -14,14 +14,19 @@ commit = self.shortRev or "dirty"; }; in - (flake-utils.lib.eachDefaultSystem (system: - { - packages.process-compose = mkPackage nixpkgs.legacyPackages.${system}; + (flake-utils.lib.eachDefaultSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + in { + packages.process-compose = mkPackage pkgs; defaultPackage = self.packages."${system}".process-compose; apps.process-compose = flake-utils.lib.mkApp { drv = self.packages."${system}".process-compose; }; apps.default = self.apps."${system}".process-compose; + checks.default = self.packages."${system}".process-compose.overrideAttrs (prev: { + doCheck = true; + nativeBuildInputs = prev.nativeBuildInputs ++ (with pkgs; [python3]); + }); }) ) // { overlays.default = final: prev: { diff --git a/src/app/commander_interface.go b/src/app/commander_interface.go index c6cfb33..41251f2 100644 --- a/src/app/commander_interface.go +++ b/src/app/commander_interface.go @@ -15,4 +15,5 @@ type Commander interface { StderrPipe() (io.ReadCloser, error) Stop(int, bool) error SetCmdArgs() + AttachIo() } diff --git a/src/app/process.go b/src/app/process.go index 167c0e9..b4b99ec 100644 --- a/src/app/process.go +++ b/src/app/process.go @@ -53,6 +53,9 @@ type Process struct { liveProber *health.Prober readyProber *health.Prober shellConfig command.ShellConfig + printLogs bool + isMain bool + extraArgs []string } func NewProcess( @@ -62,6 +65,9 @@ func NewProcess( processState *types.ProcessState, procLog *pclog.ProcessLogBuffer, shellConfig command.ShellConfig, + printLogs bool, + isMain bool, + extraArgs []string, ) *Process { colNumeric := rand.Intn(int(color.FgHiWhite)-int(color.FgHiBlack)) + int(color.FgHiBlack) @@ -78,6 +84,9 @@ func NewProcess( shellConfig: shellConfig, procStateChan: make(chan string, 1), procReadyChan: make(chan string, 1), + printLogs: printLogs, + isMain: isMain, + extraArgs: extraArgs, } proc.procReadyCtx, proc.readyCancelFn = context.WithCancel(context.Background()) @@ -92,7 +101,7 @@ func (p *Process) run() int { } if err := p.validateProcess(); err != nil { - log.Error().Err(err).Msgf("Failed to run command %s for process %s", p.getCommand(), p.getName()) + log.Error().Err(err).Msgf(`Failed to run command ["%v"] for process %s`, strings.Join(p.getCommand(), `" "`), p.getName()) p.onProcessEnd(types.ProcessStateError) return 1 } @@ -101,7 +110,7 @@ func (p *Process) run() int { for { err := p.setStateAndRun(p.getStartingStateName(), p.getProcessStarter()) if err != nil { - log.Error().Err(err).Msgf("Failed to run command %s for process %s", p.getCommand(), p.getName()) + log.Error().Err(err).Msgf(`Failed to run command ["%v"] for process %s`, strings.Join(p.getCommand(), `" "`), p.getName()) p.logBuffer.Write(err.Error()) p.onProcessEnd(types.ProcessStateError) return 1 @@ -151,16 +160,23 @@ func (p *Process) run() int { func (p *Process) getProcessStarter() func() error { return func() error { - p.command = command.BuildCommandShellArg(p.shellConfig, p.getCommand()) + p.command = command.BuildCommand( + p.procConf.Executable, + append(p.procConf.Args, p.extraArgs...), + ) p.command.SetEnv(p.getProcessEnvironment()) p.command.SetDir(p.procConf.WorkingDir) - p.command.SetCmdArgs() - stdout, _ := p.command.StdoutPipe() - stderr, _ := p.command.StderrPipe() - go p.handleOutput(stdout, p.handleInfo) - go p.handleOutput(stderr, p.handleError) - //stdin, _ := p.command.StdinPipe() - //go p.handleInput(stdin) + + if p.isMain { + p.command.AttachIo() + } else { + p.command.SetCmdArgs() + stdout, _ := p.command.StdoutPipe() + stderr, _ := p.command.StderrPipe() + go p.handleOutput(stdout, p.handleInfo) + go p.handleOutput(stderr, p.handleError) + } + return p.command.Start() } } @@ -346,8 +362,11 @@ func (p *Process) getNameWithSmartReplica() string { return p.procConf.Name } -func (p *Process) getCommand() string { - return p.procConf.Command +func (p *Process) getCommand() []string { + return append( + []string{(*p.procConf).Executable}, + append(p.procConf.Args, p.extraArgs...)..., + ) } func (p *Process) updateProcState() { @@ -401,13 +420,17 @@ func (p *Process) handleOutput(pipe io.ReadCloser, handler func(message string)) func (p *Process) handleInfo(message string) { p.logger.Info(message, p.getName(), p.procConf.ReplicaNum) - fmt.Printf("[%s\t] %s\n", p.procColor(p.getName()), message) + if p.printLogs { + fmt.Printf("[%s\t] %s\n", p.procColor(p.getName()), message) + } p.logBuffer.Write(message) } func (p *Process) handleError(message string) { p.logger.Error(message, p.getName(), p.procConf.ReplicaNum) - fmt.Printf("[%s\t] %s\n", p.procColor(p.getName()), p.redColor(message)) + if p.printLogs { + fmt.Printf("[%s\t] %s\n", p.procColor(p.getName()), p.redColor(message)) + } p.logBuffer.Write(message) } diff --git a/src/app/project_runner.go b/src/app/project_runner.go index 84afda4..a45589f 100644 --- a/src/app/project_runner.go +++ b/src/app/project_runner.go @@ -25,6 +25,8 @@ type ProjectRunner struct { logger pclog.PcLogger waitGroup sync.WaitGroup exitCode int + mainProcess string + mainProcessArgs []string } func (p *ProjectRunner) GetLexicographicProcessNames() ([]string, error) { @@ -83,7 +85,25 @@ func (p *ProjectRunner) runProcess(config *types.ProcessConfig) { procLog = pclog.NewLogBuffer(0) } procState, _ := p.GetProcessState(config.ReplicaName) - process := NewProcess(p.project.Environment, procLogger, config, procState, procLog, *p.project.ShellConfig) + isMain := config.Name == p.mainProcess + hasMain := p.mainProcess != "" + printLogs := !hasMain + extraArgs := []string{} + if isMain { + extraArgs = p.mainProcessArgs + config.RestartPolicy.ExitOnEnd = true + } + process := NewProcess( + p.project.Environment, + procLogger, + config, + procState, + procLog, + *p.project.ShellConfig, + printLogs, + isMain, + extraArgs, + ) p.addRunningProcess(process) p.waitGroup.Add(1) go func() { @@ -561,10 +581,18 @@ func (p *ProjectRunner) GetProject() *types.Project { return p.project } -func NewProjectRunner(project *types.Project, processesToRun []string, noDeps bool) (*ProjectRunner, error) { +func NewProjectRunner( + project *types.Project, + processesToRun []string, + noDeps bool, + mainProcess string, + mainProcessArgs []string, +) (*ProjectRunner, error) { runner := &ProjectRunner{ project: project, + mainProcess: mainProcess, + mainProcessArgs: mainProcessArgs, } var err error diff --git a/src/app/system_test.go b/src/app/system_test.go index a5682e0..4496f8d 100644 --- a/src/app/system_test.go +++ b/src/app/system_test.go @@ -28,7 +28,7 @@ func TestSystem_TestFixtures(t *testing.T) { t.Errorf(err.Error()) return } - runner, err := NewProjectRunner(project, []string{}, false) + runner, err := NewProjectRunner(project, []string{}, false, "", []string{}) if err != nil { t.Errorf(err.Error()) return @@ -48,7 +48,7 @@ func TestSystem_TestComposeWithLog(t *testing.T) { t.Errorf(err.Error()) return } - runner, err := NewProjectRunner(project, []string{}, false) + runner, err := NewProjectRunner(project, []string{}, false, "", []string{}) if err != nil { t.Errorf(err.Error()) return @@ -81,7 +81,7 @@ func TestSystem_TestComposeChain(t *testing.T) { t.Errorf(err.Error()) return } - runner, err := NewProjectRunner(project, []string{}, false) + runner, err := NewProjectRunner(project, []string{}, false, "", []string{}) if err != nil { t.Errorf(err.Error()) return @@ -117,7 +117,7 @@ func TestSystem_TestComposeChainExit(t *testing.T) { t.Errorf(err.Error()) return } - runner, err := NewProjectRunner(project, []string{}, false) + runner, err := NewProjectRunner(project, []string{}, false, "", []string{}) if err != nil { t.Errorf(err.Error()) return @@ -162,7 +162,7 @@ func TestSystem_TestComposeScale(t *testing.T) { t.Errorf(err.Error()) return } - runner, err := NewProjectRunner(project, []string{}, false) + runner, err := NewProjectRunner(project, []string{}, false, "", []string{}) if err != nil { t.Errorf(err.Error()) return diff --git a/src/cmd/project_runner.go b/src/cmd/project_runner.go index d3cfad2..5c53745 100644 --- a/src/cmd/project_runner.go +++ b/src/cmd/project_runner.go @@ -13,7 +13,7 @@ import ( "time" ) -func getProjectRunner(process []string, noDeps bool) *app.ProjectRunner { +func getProjectRunner(process []string, noDeps bool, mainProcess string, mainProcessArgs []string) *app.ProjectRunner { if *pcFlags.HideDisabled { opts.AddAdmitter(&admitter.DisabledProcAdmitter{}) } @@ -23,7 +23,7 @@ func getProjectRunner(process []string, noDeps bool) *app.ProjectRunner { log.Fatal().Msg(err.Error()) } - runner, err := app.NewProjectRunner(project, process, noDeps) + runner, err := app.NewProjectRunner(project, process, noDeps, mainProcess, mainProcessArgs) if err != nil { fmt.Println(err) log.Fatal().Msg(err.Error()) diff --git a/src/cmd/root.go b/src/cmd/root.go index a54e3b8..2e284c7 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -34,7 +34,7 @@ func run(cmd *cobra.Command, args []string) error { defer func() { _ = logFile.Close() }() - runner := getProjectRunner([]string{}, false) + runner := getProjectRunner([]string{}, false, "", []string{}) api.StartHttpServer(!*pcFlags.Headless, *pcFlags.PortNum, runner) runProject(runner) return nil diff --git a/src/cmd/run.go b/src/cmd/run.go new file mode 100644 index 0000000..15dfdc4 --- /dev/null +++ b/src/cmd/run.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "fmt" + "os" + "github.com/f1bonacc1/process-compose/src/api" + "github.com/spf13/cobra" +) + +// runCmd represents the up command +var runCmd = &cobra.Command{ + Use: "run PROCESS [flags] -- [process_args]", + Short: "Run PROCESS in the foreground, and its dependencies in the background", + Long: `Run selected process with std(in|out|err) attached, while other processes run in the background. +Command line arguments, provided after --, are passed to the PROCESS.`, + Args: cobra.MinimumNArgs(1), + // Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + *pcFlags.Headless = false + + processName := args[0] + + if len(args) > 1 { + argsLenAtDash := cmd.ArgsLenAtDash() + if argsLenAtDash != 1 { + message := "Extra positional arguments provided! To pass args to PROCESS, separate them from process-compose arguments with: --" + fmt.Println(message) + os.Exit(1) + } + args = args[argsLenAtDash:] + } else { + // Clease args as they will contain the processName + args = []string{} + } + + runner := getProjectRunner( + []string{processName}, + *pcFlags.NoDependencies, + processName, + args, + ) + + api.StartHttpServer(false, *pcFlags.PortNum, runner) + runProject(runner) + }, +} + +func init() { + rootCmd.AddCommand(runCmd) + + runCmd.Flags().BoolVarP(pcFlags.NoDependencies, "no-deps", "", *pcFlags.NoDependencies, "don't start dependent processes") + runCmd.Flags().AddFlag(rootCmd.Flags().Lookup("config")) + +} diff --git a/src/cmd/up.go b/src/cmd/up.go index 9e51859..bc85947 100644 --- a/src/cmd/up.go +++ b/src/cmd/up.go @@ -14,7 +14,7 @@ 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) { - runner := getProjectRunner(args, *pcFlags.NoDependencies) + runner := getProjectRunner(args, *pcFlags.NoDependencies, "", []string{}) api.StartHttpServer(!*pcFlags.Headless, *pcFlags.PortNum, runner) runProject(runner) }, diff --git a/src/command/command.go b/src/command/command.go index 3798202..4355fa8 100644 --- a/src/command/command.go +++ b/src/command/command.go @@ -8,9 +8,10 @@ import ( "runtime" ) -func BuildCommandShellArg(shell ShellConfig, cmd string) *CmdWrapper { + +func BuildCommand(cmd string, args []string) *CmdWrapper { return &CmdWrapper{ - cmd: exec.Command(shell.ShellCommand, shell.ShellArgument, cmd), + cmd: exec.Command(cmd, args...), } //return NewMockCommand() } diff --git a/src/command/command_wrapper.go b/src/command/command_wrapper.go index 9b4ce5a..04f4e13 100644 --- a/src/command/command_wrapper.go +++ b/src/command/command_wrapper.go @@ -2,6 +2,7 @@ package command import ( "io" + "os" "os/exec" ) @@ -37,6 +38,12 @@ func (c *CmdWrapper) StderrPipe() (io.ReadCloser, error) { return c.cmd.StderrPipe() } +func (c *CmdWrapper) AttachIo() () { + c.cmd.Stdin = os.Stdin + c.cmd.Stdout = os.Stdout + c.cmd.Stderr = os.Stderr +} + func (c *CmdWrapper) SetEnv(env []string) { c.cmd.Env = env } diff --git a/src/tui/proc-info-form.go b/src/tui/proc-info-form.go index cbc379e..868ee51 100644 --- a/src/tui/proc-info-form.go +++ b/src/tui/proc-info-form.go @@ -19,10 +19,10 @@ func (pv *pcView) createProcInfoForm(info *types.ProcessConfig, ports *types.Pro f.SetFieldTextColor(tcell.ColorBlack) f.SetButtonsAlign(tview.AlignCenter) f.SetTitle("Process " + info.Name + " Info") + addStringIfNotEmpty("Entrypoint:", strings.Join(info.Entrypoint, " "), f) addStringIfNotEmpty("Command:", info.Command, f) addStringIfNotEmpty("Working Directory:", info.WorkingDir, f) addStringIfNotEmpty("Log Location:", info.LogLocation, f) - f.AddInputField("Replica:", fmt.Sprintf("%d/%d", info.ReplicaNum+1, info.Replicas), 0, nil, nil) addDropDownIfNotEmpty("Environment:", info.Environment, f) addCSVIfNotEmpty("Depends On:", mapKeysToSlice(info.DependsOn), f) if ports != nil { diff --git a/src/types/process.go b/src/types/process.go index 55fbb3c..fb0073c 100644 --- a/src/types/process.go +++ b/src/types/process.go @@ -16,6 +16,7 @@ type ProcessConfig struct { Disabled bool `yaml:"disabled,omitempty"` IsDaemon bool `yaml:"is_daemon,omitempty"` Command string `yaml:"command"` + Entrypoint []string `yaml:"entrypoint"` LogLocation string `yaml:"log_location,omitempty"` LoggerConfig *LoggerConfig `yaml:"log_configuration,omitempty"` Environment Environment `yaml:"environment,omitempty"` @@ -31,6 +32,8 @@ type ProcessConfig struct { Extensions map[string]interface{} `yaml:",inline"` ReplicaNum int ReplicaName string + Executable string + Args []string } func (p *ProcessConfig) GetDependencies() []string { diff --git a/src/types/validators.go b/src/types/validators.go index 8454d3a..dcbb21c 100644 --- a/src/types/validators.go +++ b/src/types/validators.go @@ -5,6 +5,7 @@ import ( "github.com/f1bonacc1/process-compose/src/command" "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "os" "strings" ) @@ -19,6 +20,7 @@ func (p *Project) ValidateAfterMerge() error { p.assignDefaultProcessValues() p.cloneReplicas() p.copyWorkingDirToProbes() + p.validateProcessCommand() return p.validateNoCircularDependencies() } @@ -74,6 +76,32 @@ func (p *Project) assignDefaultProcessValues() { } } +func (p *Project) validateProcessCommand() { + for name, proc := range p.Processes { + command := proc.Command + entrypoint := proc.Entrypoint + + if command != "" || len(entrypoint) == 0 { + if len(entrypoint) > 0 { + message := fmt.Sprintf("Both command and entrypoint are set! Using command and ignoring entrypoint (procces: %s)", name) + fmt.Fprintln(os.Stderr, "process-compose:", message) + log.Error().Msg(message) + } + + proc.Executable = p.ShellConfig.ShellCommand; + proc.Args = []string{ + p.ShellConfig.ShellArgument, + command, + } + } else { + proc.Executable = entrypoint[0] + proc.Args = entrypoint[1:] + } + + p.Processes[name] = proc + } +} + func (p *Project) copyWorkingDirToProbes() { for name, proc := range p.Processes { if proc.LivenessProbe != nil &&