diff --git a/src/api/pc_api.go b/src/api/pc_api.go index 067adb9d..86e0f8a3 100644 --- a/src/api/pc_api.go +++ b/src/api/pc_api.go @@ -301,6 +301,27 @@ func (api *PcApi) UpdateProject(c *gin.Context) { c.JSON(http.StatusOK, status) } +// @Schemes +// @Description Update porcess +// @Tags Process +// @Summary Updates process configuration +// @Produce json +// @Success 200 +// @Router /process [post] +func (api *PcApi) UpdateProcess(c *gin.Context) { + var proc types.ProcessConfig + if err := c.ShouldBindJSON(&proc); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + err := api.project.UpdateProcess(&proc) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, proc) +} + // @Schemes // @Description Retrieves project state information // @Tags Project diff --git a/src/api/routes.go b/src/api/routes.go index 069aa9e5..1608c6c0 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -43,6 +43,7 @@ func InitRoutes(useLogger bool, handler *PcApi) *gin.Engine { r.GET("/processes", handler.GetProcesses) r.GET("/process/:name", handler.GetProcess) r.GET("/process/info/:name", handler.GetProcessInfo) + r.POST("/process", handler.UpdateProcess) r.GET("/process/ports/:name", handler.GetProcessPorts) r.GET("/process/logs/:name/:endOffset/:limit", handler.GetProcessLogs) r.PATCH("/process/stop/:name", handler.StopProcess) diff --git a/src/app/project_interface.go b/src/app/project_interface.go index c02308fe..5ba234c2 100644 --- a/src/app/project_interface.go +++ b/src/app/project_interface.go @@ -30,4 +30,5 @@ type IProject interface { GetProcessPorts(name string) (*types.ProcessPorts, error) SetProcessPassword(name string, password string) error UpdateProject(project *types.Project) (map[string]string, error) + UpdateProcess(updated *types.ProcessConfig) error } diff --git a/src/app/project_runner.go b/src/app/project_runner.go index c27ed1bb..e5956e47 100644 --- a/src/app/project_runner.go +++ b/src/app/project_runner.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "github.com/f1bonacc1/process-compose/src/config" + "github.com/f1bonacc1/process-compose/src/health" "github.com/f1bonacc1/process-compose/src/pclog" "github.com/f1bonacc1/process-compose/src/types" "os" @@ -579,13 +580,14 @@ func (p *ProjectRunner) ScaleProcess(name string, scale int) error { return err } if processConfig, ok := p.project.Processes[name]; ok { - scaleDelta := scale - processConfig.Replicas + origScale := p.getCurrentReplicaCount(processConfig.Name) + scaleDelta := scale - origScale if scaleDelta < 0 { - log.Info().Msgf("scaling down %s by %d", name, scaleDelta*-1) + log.Info().Msgf("scaling down %s by %d", name, -scaleDelta) p.scaleDownProcess(processConfig.Name, scale) } else if scaleDelta > 0 { log.Info().Msgf("scaling up %s by %d", name, scaleDelta) - p.scaleUpProcess(processConfig, scaleDelta, scale) + p.scaleUpProcess(processConfig, scaleDelta, scale, origScale) } else { log.Info().Msgf("no change in scale of %s", name) return nil @@ -597,8 +599,17 @@ func (p *ProjectRunner) ScaleProcess(name string, scale int) error { return nil } -func (p *ProjectRunner) scaleUpProcess(proc types.ProcessConfig, toAdd, scale int) { - origScale := proc.Replicas +func (p *ProjectRunner) getCurrentReplicaCount(name string) int { + counter := 0 + for _, proc := range p.project.Processes { + if proc.Name == name { + counter++ + } + } + return counter +} + +func (p *ProjectRunner) scaleUpProcess(proc types.ProcessConfig, toAdd, scale, origScale int) { for i := 0; i < toAdd; i++ { proc.ReplicaNum = origScale + i proc.Replicas = scale @@ -708,7 +719,9 @@ func (p *ProjectRunner) addProcessAndRun(proc types.ProcessConfig) { p.statesMutex.Unlock() p.project.Processes[proc.ReplicaName] = proc p.initProcessLog(proc.ReplicaName) - p.runProcess(&proc) + if !proc.Disabled { + p.runProcess(&proc) + } } func (p *ProjectRunner) selectRunningProcesses(procList []string) error { @@ -887,15 +900,58 @@ func (p *ProjectRunner) UpdateProject(project *types.Project) (map[string]string } //Update processes for name, proc := range updatedProcs { - err := p.removeProcess(name) + err := p.UpdateProcess(&proc) if err != nil { - log.Err(err).Msgf("Failed to remove process %s", name) + log.Err(err).Msgf("Failed to update process %s", name) errs = append(errs, err) status[name] = types.ProcessUpdateError continue } - p.addProcessAndRun(proc) status[name] = types.ProcessUpdateUpdated } return status, errors.Join(errs...) } + +func (p *ProjectRunner) UpdateProcess(updated *types.ProcessConfig) error { + isScaleChanged := false + validateProbes(updated.LivenessProbe) + validateProbes(updated.ReadinessProbe) + updated.AssignProcessExecutableAndArgs(p.project.ShellConfig, p.project.ShellConfig.ElevatedShellArg) + if currentProc, ok := p.project.Processes[updated.ReplicaName]; ok { + equal := currentProc.Compare(updated) + if equal { + log.Debug().Msgf("Process %s is up to date", updated.Name) + return nil + } + log.Debug().Msgf("Process %s is updated", updated.Name) + if currentProc.Replicas != updated.Replicas { + isScaleChanged = true + } + } else { + err := fmt.Errorf("no such process: %s", updated.ReplicaName) + log.Err(err).Msgf("Failed to update process %s", updated.ReplicaName) + return err + } + + err := p.removeProcess(updated.ReplicaName) + if err != nil { + log.Err(err).Msgf("Failed to remove process %s", updated.ReplicaName) + return err + } + p.addProcessAndRun(*updated) + + if isScaleChanged { + err = p.ScaleProcess(updated.ReplicaName, updated.Replicas) + if err != nil { + log.Err(err).Msgf("Failed to scale process %s", updated.Name) + return err + } + } + return nil +} + +func validateProbes(probe *health.Probe) { + if probe != nil { + probe.ValidateAndSetDefaults() + } +} diff --git a/src/app/system_test.go b/src/app/system_test.go index 19dbe238..247d5bdc 100644 --- a/src/app/system_test.go +++ b/src/app/system_test.go @@ -650,7 +650,7 @@ func TestUpdateProject(t *testing.T) { project: &types.Project{ ShellConfig: shell, Processes: map[string]types.ProcessConfig{ - "process1": { + proc1: { Name: proc1, ReplicaName: proc1, Executable: shell.ShellCommand, @@ -660,7 +660,7 @@ func TestUpdateProject(t *testing.T) { "VAR2=value2", }, }, - "process2": { + proc2: { Name: proc2, ReplicaName: proc2, Executable: shell.ShellCommand, @@ -684,7 +684,7 @@ func TestUpdateProject(t *testing.T) { project := &types.Project{ ShellConfig: shell, Processes: map[string]types.ProcessConfig{ - "process1": { + proc1: { Name: proc1, ReplicaName: proc1, Executable: shell.ShellCommand, @@ -694,7 +694,7 @@ func TestUpdateProject(t *testing.T) { "VAR2=value2", }, }, - "process2": { + proc2: { Name: proc2, ReplicaName: proc2, Executable: shell.ShellCommand, @@ -718,7 +718,7 @@ func TestUpdateProject(t *testing.T) { project = &types.Project{ ShellConfig: shell, Processes: map[string]types.ProcessConfig{ - "process1": { + proc1: { Name: proc1, ReplicaName: proc1, Executable: shell.ShellCommand, @@ -728,11 +728,10 @@ func TestUpdateProject(t *testing.T) { "VAR2=value2", }, }, - "process2": { + proc2: { Name: proc2, ReplicaName: proc2, - Executable: shell.ShellCommand, - Args: []string{shell.ShellArgument, "echo process2"}, + Command: "echo process2 updated", Environment: []string{ "VAR3=value3", "VAR4=value4", @@ -756,10 +755,22 @@ func TestUpdateProject(t *testing.T) { t.Errorf("Process 'process1' status is %s want %s", updatedStatus, types.ProcessUpdateUpdated) } + proc, ok = p.project.Processes[proc2] + if !ok { + t.Errorf("Process 'process2' not found in updated project") + } + if proc.Args[1] != "echo process2 updated" { + t.Errorf("Process 'process2' command is %s want 'echo process2 updated'", proc.Args[1]) + } + updatedStatus = status[proc2] + if updatedStatus != types.ProcessUpdateUpdated { + t.Errorf("Process 'process2' status is %s want %s", updatedStatus, types.ProcessUpdateUpdated) + } + // Test when a process is deleted project = &types.Project{ Processes: map[string]types.ProcessConfig{ - "process2": { + proc2: { Name: proc2, ReplicaName: proc2, Executable: shell.ShellCommand, diff --git a/src/client/client.go b/src/client/client.go index a2e2be63..58552861 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -167,3 +167,7 @@ func (p *PcClient) SetProcessPassword(_, _ string) error { func (p *PcClient) UpdateProject(project *types.Project) (map[string]string, error) { return p.updateProject(project) } + +func (p *PcClient) UpdateProcess(updated *types.ProcessConfig) error { + return p.updateProcess(updated) +} diff --git a/src/client/processes.go b/src/client/processes.go index 13ffd048..f4d3772b 100644 --- a/src/client/processes.go +++ b/src/client/processes.go @@ -1,10 +1,12 @@ package client import ( + "bytes" "encoding/json" "fmt" "github.com/f1bonacc1/process-compose/src/types" "github.com/rs/zerolog/log" + "net/http" "sort" ) @@ -92,3 +94,27 @@ func (p *PcClient) getProcessPorts(name string) (*types.ProcessPorts, error) { return &sResp, nil } + +func (p *PcClient) updateProcess(procInfo *types.ProcessConfig) error { + url := fmt.Sprintf("http://%s/process", p.address) + jsonData, err := json.Marshal(procInfo) + if err != nil { + log.Err(err).Msg("failed to marshal process") + return err + } + resp, err := p.client.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + log.Err(err).Msg("failed to update process") + return err + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return nil + } + var respErr pcError + if err = json.NewDecoder(resp.Body).Decode(&respErr); err != nil { + log.Err(err).Msg("failed to decode err update process") + return err + } + return fmt.Errorf(respErr.Error) +} diff --git a/src/cmd/root.go b/src/cmd/root.go index 28f6429a..fb21eed3 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "errors" "fmt" "github.com/f1bonacc1/process-compose/src/admitter" "github.com/f1bonacc1/process-compose/src/api" @@ -154,7 +155,8 @@ func setupLogger() *os.File { func handleErrorAndExit(err error) { if err != nil { log.Error().Err(err) - if exitErr, ok := err.(*app.ExitError); ok { + var exitErr *app.ExitError + if errors.As(err, &exitErr) { os.Exit(exitErr.Code) } os.Exit(1) diff --git a/src/docs/docs.go b/src/docs/docs.go index 06f393f5..23ff6931 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -50,6 +50,23 @@ const docTemplate = `{ } } }, + "/process": { + "post": { + "description": "Update porcess", + "produces": [ + "application/json" + ], + "tags": [ + "Process" + ], + "summary": "Updates process configuration", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/process/info/{name}": { "get": { "description": "Retrieves the given process and its config", diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 6c88715d..cb797f9a 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -38,6 +38,23 @@ } } }, + "/process": { + "post": { + "description": "Update porcess", + "produces": [ + "application/json" + ], + "tags": [ + "Process" + ], + "summary": "Updates process configuration", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/process/info/{name}": { "get": { "description": "Retrieves the given process and its config", diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index cd01c8ff..7bf882aa 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -23,6 +23,17 @@ paths: summary: Liveness Check tags: - Liveness + /process: + post: + description: Update porcess + produces: + - application/json + responses: + "200": + description: OK + summary: Updates process configuration + tags: + - Process /process/{name}: get: description: Retrieves the given process and its status diff --git a/src/loader/mutators.go b/src/loader/mutators.go index 2845ec7b..99b1d004 100644 --- a/src/loader/mutators.go +++ b/src/loader/mutators.go @@ -7,7 +7,6 @@ import ( "github.com/f1bonacc1/process-compose/src/templater" "github.com/f1bonacc1/process-compose/src/types" "github.com/rs/zerolog/log" - "os" ) type mutatorFunc func(p *types.Project) @@ -96,33 +95,12 @@ func cloneReplicas(p *types.Project) { } func assignExecutableAndArgs(p *types.Project) { + elevatedShellArg := p.ShellConfig.ElevatedShellArg + if p.IsTuiDisabled { + elevatedShellArg = "" + } for name, proc := range p.Processes { - elevatedShellArg := p.ShellConfig.ElevatedShellArg - if p.IsTuiDisabled { - elevatedShellArg = "" - } - if proc.Command != "" || len(proc.Entrypoint) == 0 { - if len(proc.Entrypoint) > 0 { - message := fmt.Sprintf("'command' and 'entrypoint' are set! Using command (process: %s)", name) - _, _ = fmt.Fprintln(os.Stderr, "process-compose:", message) - log.Warn().Msg(message) - } - - proc.Executable = p.ShellConfig.ShellCommand - - if proc.IsElevated { - proc.Args = []string{p.ShellConfig.ShellArgument, fmt.Sprintf("%s %s %s", p.ShellConfig.ElevatedShellCmd, elevatedShellArg, proc.Command)} - } else { - proc.Args = []string{p.ShellConfig.ShellArgument, proc.Command} - } - } else { - if proc.IsElevated { - proc.Entrypoint = append([]string{p.ShellConfig.ElevatedShellCmd, elevatedShellArg}, proc.Entrypoint...) - } - proc.Executable = proc.Entrypoint[0] - proc.Args = proc.Entrypoint[1:] - - } + proc.AssignProcessExecutableAndArgs(p.ShellConfig, elevatedShellArg) p.Processes[name] = proc } diff --git a/src/loader/validators.go b/src/loader/validators.go index 3de46d68..ef4737e7 100644 --- a/src/loader/validators.go +++ b/src/loader/validators.go @@ -7,7 +7,6 @@ import ( "github.com/rs/zerolog/log" "os/exec" "runtime" - "strings" ) type validatorFunc func(p *types.Project) error @@ -38,19 +37,13 @@ func validateLogLevel(p *types.Project) error { } func validateProcessConfig(p *types.Project) error { - for key, proc := range p.Processes { - if len(proc.Extensions) == 0 { - continue - } - for extKey := range proc.Extensions { - if strings.HasPrefix(extKey, "x-") { - continue - } - errStr := fmt.Sprintf("unknown key '%s' found in process '%s'", extKey, key) + for _, proc := range p.Processes { + err := proc.ValidateProcessConfig() + if err != nil { + log.Err(err).Msgf("Process config validation failed") if p.IsStrict { - return fmt.Errorf(errStr) + return err } - log.Error().Msgf(errStr) } } return nil diff --git a/src/tui/actions.go b/src/tui/actions.go index 34c19a2a..2cf97f5b 100644 --- a/src/tui/actions.go +++ b/src/tui/actions.go @@ -39,6 +39,7 @@ const ( ActionFocusChange = ActionName("focus_change") ActionClearLog = ActionName("clear_log") ActionMarkLog = ActionName("mark_log") + ActionEditProcess = ActionName("edit_process") ) var defaultShortcuts = map[ActionName]tcell.Key{ @@ -67,6 +68,7 @@ var defaultShortcuts = map[ActionName]tcell.Key{ ActionFocusChange: tcell.KeyTab, ActionClearLog: tcell.KeyCtrlK, ActionMarkLog: tcell.KeyRune, + ActionEditProcess: tcell.KeyCtrlE, } var defaultShortcutsRunes = map[ActionName]rune{ @@ -99,6 +101,7 @@ var procActionsOrder = []ActionName{ ActionProcessScreen, ActionProcessStop, ActionProcessRestart, + ActionEditProcess, ActionNsFilter, ActionHideDisabled, ActionQuit, @@ -359,6 +362,9 @@ func newShortCuts() *ShortCuts { ActionMarkLog: { Description: "Add Mark to Log", }, + ActionEditProcess: { + Description: "Edit Process", + }, }, } for k, v := range sc.ShortCutKeys { diff --git a/src/tui/proc-editor.go b/src/tui/proc-editor.go new file mode 100644 index 00000000..9271daa8 --- /dev/null +++ b/src/tui/proc-editor.go @@ -0,0 +1,134 @@ +package tui + +import ( + "errors" + "fmt" + "github.com/f1bonacc1/process-compose/src/types" + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v3" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +type dataError struct { + err error +} + +func (de *dataError) Error() string { + return fmt.Sprintf("# Error unmarshaling YAML %v\n# Please apply the changes again and save the file.\n\n", de.err) +} + +func (pv *pcView) editSelectedProcess() { + editor, err := findTextEditor() + if err != nil { + pv.showError(err.Error()) + return + } + + name := pv.getSelectedProcName() + info, err := pv.project.GetProcessInfo(name) + if err != nil { + pv.showError(err.Error()) + return + } + tmpDir := os.TempDir() + filename := filepath.Join(tmpDir, fmt.Sprintf("pc-%s-config.yaml", name)) + err = writeProcInfoToFile(info, filename) + if err != nil { + pv.showError(fmt.Sprintf("Failed to write process to file: %s - %v", filename, err.Error())) + return + } + //remove file after closing the editor + defer os.Remove(filename) + var updatedProc *types.ProcessConfig + for { + pv.runCommandInForeground(editor, filename) + updatedProc, err = loadProcInfoFromFile(filename) + if err != nil { + var de *dataError + if errors.As(err, &de) { + err = writeProcInfoToFile(info, filename, de.Error()) + if err != nil { + pv.showError(fmt.Sprintf("Failed to write process to file: %s - %v", filename, err.Error())) + return + } + continue + } + pv.showError(fmt.Sprintf("Failed to load process from file: %s - %v", filename, err.Error())) + return + } + break + } + if updatedProc != nil { + err = pv.project.UpdateProcess(updatedProc) + if err != nil { + pv.showError(fmt.Sprintf("Failed to update process: %s - %v", name, err.Error())) + return + } + } +} + +func writeProcInfoToFile(info *types.ProcessConfig, filename string, optionalPrefix ...string) error { + yamlData, err := yaml.Marshal(info) + if err != nil { + log.Err(err).Msgf("Failed to marshal file %s", filename) + return err + } + for _, prefix := range optionalPrefix { + yamlData = append([]byte(prefix), yamlData...) + } + + // Write YAML to file + err = os.WriteFile(filename, yamlData, 0600) + if err != nil { + log.Err(err).Msgf("Failed to write file %s", filename) + return err + } + + return nil +} + +func loadProcInfoFromFile(filename string) (*types.ProcessConfig, error) { + yamlFile, err := os.ReadFile(filename) + if err != nil { + log.Err(err).Msgf("Failed to read file %s", filename) + return nil, err + } + info := &types.ProcessConfig{} + err = yaml.Unmarshal(yamlFile, info) + if err != nil { + log.Err(err).Msgf("Failed to unmarshal file %s", filename) + return nil, &dataError{err: err} + } + err = info.ValidateProcessConfig() + if err != nil { + return nil, &dataError{err: err} + } + return info, nil +} + +func findTextEditor() (string, error) { + // Check if $EDITOR environment variable is set + if editor := os.Getenv("EDITOR"); editor != "" { + return editor, nil + } + + // List of popular text editors to check + var editors []string + if runtime.GOOS == "windows" { + editors = []string{"notepad.exe", "notepad++.exe", "code.exe", "gvim.exe"} + } else { + editors = []string{"vim", "nano", "vi"} + } + + for _, editor := range editors { + path, err := exec.LookPath(editor) + if err == nil { + return path, nil + } + } + + return "", fmt.Errorf("no text editor found") +} diff --git a/src/tui/proc-info-form.go b/src/tui/proc-info-form.go index 7bed0e76..e3cabbf0 100644 --- a/src/tui/proc-info-form.go +++ b/src/tui/proc-info-form.go @@ -27,7 +27,6 @@ func (pv *pcView) createProcInfoForm(info *types.ProcessConfig, ports *types.Pro if ports != nil { addCSVIfNotEmpty("TCP Ports:", ports.TcpPorts, f) } - f.AddInputField("Replica:", fmt.Sprintf("%d/%d", info.ReplicaNum+1, info.Replicas), 0, nil, nil) f.AddCheckbox("Is Disabled:", info.Disabled, nil) f.AddCheckbox("Is Daemon:", info.IsDaemon, nil) f.AddCheckbox("Is TTY:", info.IsTty, nil) diff --git a/src/tui/proc-starter.go b/src/tui/proc-starter.go index f598d761..80e47661 100644 --- a/src/tui/proc-starter.go +++ b/src/tui/proc-starter.go @@ -40,6 +40,17 @@ func (pv *pcView) runShellProcess() { pv.runForeground(shellCmd) } +func (pv *pcView) runCommandInForeground(cmd string, args ...string) { + shellCmd := &types.ProcessConfig{ + Executable: cmd, + Args: args, + RestartPolicy: types.RestartPolicyConfig{ + Restart: types.RestartPolicyNo, + }, + } + pv.runForeground(shellCmd) +} + func (pv *pcView) runForeground(info *types.ProcessConfig) bool { pv.halt() defer pv.resume() diff --git a/src/tui/view.go b/src/tui/view.go index 0884c8b8..df5b295c 100644 --- a/src/tui/view.go +++ b/src/tui/view.go @@ -239,6 +239,9 @@ func (pv *pcView) setShortCutsActions() { pv.shortcuts.setAction(ActionMarkLog, func() { pv.logsText.AddMark() }) + pv.shortcuts.setAction(ActionEditProcess, func() { + pv.editSelectedProcess() + }) } func (pv *pcView) setFullScreen(isFullScreen bool) { diff --git a/src/types/logger.go b/src/types/logger.go index e4d42dd7..bc924b4e 100644 --- a/src/types/logger.go +++ b/src/types/logger.go @@ -21,7 +21,7 @@ type LoggerConfig struct { // Rotation is the configuration for logging rotation Rotation *LogRotationConfig `yaml:"rotation"` // FieldsOrder is the order in which fields are logged - FieldsOrder []string `yaml:"fields_order"` + FieldsOrder []string `yaml:"fields_order,omitempty"` // DisableJSON disables log JSON formatting DisableJSON bool `yaml:"disable_json"` // TimestampFormat is the format of the timestamp diff --git a/src/types/process.go b/src/types/process.go index b8b2de44..7606afc8 100644 --- a/src/types/process.go +++ b/src/types/process.go @@ -2,9 +2,13 @@ package types import ( "fmt" + "github.com/f1bonacc1/process-compose/src/command" "github.com/f1bonacc1/process-compose/src/health" + "github.com/rs/zerolog/log" "math" + "os" "reflect" + "strings" "time" ) @@ -18,7 +22,7 @@ type ProcessConfig struct { Disabled bool `yaml:"disabled,omitempty"` IsDaemon bool `yaml:"is_daemon,omitempty"` Command string `yaml:"command"` - Entrypoint []string `yaml:"entrypoint"` + Entrypoint []string `yaml:"entrypoint,omitempty"` LogLocation string `yaml:"log_location,omitempty"` LoggerConfig *LoggerConfig `yaml:"log_configuration,omitempty"` Environment Environment `yaml:"environment,omitempty"` @@ -34,7 +38,7 @@ type ProcessConfig struct { Replicas int `yaml:"replicas"` Extensions map[string]interface{} `yaml:",inline"` Description string `yaml:"description,omitempty"` - Vars Vars `yaml:"vars"` + Vars Vars `yaml:"vars,omitempty"` IsForeground bool `yaml:"is_foreground"` IsTty bool `yaml:"is_tty"` IsElevated bool `yaml:"is_elevated"` @@ -101,11 +105,53 @@ func (p *ProcessConfig) Compare(another *ProcessConfig) bool { !reflect.DeepEqual(p.RestartPolicy, another.RestartPolicy) || !reflect.DeepEqual(p.Environment, another.Environment) || !reflect.DeepEqual(p.Args, another.Args) { + //diffs := compareStructs(*p, *another) + //log.Warn().Msgf("Structs are different: %s", diffs) return false } return true } +func (p *ProcessConfig) AssignProcessExecutableAndArgs(shellConf *command.ShellConfig, elevatedShellArg string) { + if p.Command != "" || len(p.Entrypoint) == 0 { + if len(p.Entrypoint) > 0 { + message := fmt.Sprintf("'command' and 'entrypoint' are set! Using command (process: %s)", p.Name) + _, _ = fmt.Fprintln(os.Stderr, "process-compose:", message) + log.Warn().Msg(message) + } + + p.Executable = shellConf.ShellCommand + + if len(p.Command) == 0 { + return + } + if p.IsElevated { + p.Args = []string{shellConf.ShellArgument, fmt.Sprintf("%s %s %s", shellConf.ElevatedShellCmd, elevatedShellArg, p.Command)} + } else { + p.Args = []string{shellConf.ShellArgument, p.Command} + } + } else { + if p.IsElevated { + p.Entrypoint = append([]string{shellConf.ElevatedShellCmd, elevatedShellArg}, p.Entrypoint...) + } + p.Executable = p.Entrypoint[0] + p.Args = p.Entrypoint[1:] + } +} + +func (p *ProcessConfig) ValidateProcessConfig() error { + if len(p.Extensions) == 0 { + return nil // no error + } + for extKey := range p.Extensions { + if strings.HasPrefix(extKey, "x-") { + continue + } + return fmt.Errorf("unknown key '%s' found in process '%s'", extKey, p.Name) + } + + return nil +} func compareStructs(a, b interface{}) []string { var differences []string diff --git a/www/docs/configuration.md b/www/docs/configuration.md index c8303510..9ebbc225 100644 --- a/www/docs/configuration.md +++ b/www/docs/configuration.md @@ -224,6 +224,45 @@ Using multiple `process-compose` files lets you customize a `process-compose` ap See the [Merging Configuration](merge.md) for more information on merging files. +## On the Fly Configuration Edit + +Process Compose allows you to edit processes configuration without restarting the entire project. To achieve that, select one of the following options: + +### Project Edit + +Modify your `process-compose.yaml` file (or files) and apply the changes by running: + +```shell +process-compose project update -f process-compose.yaml # add -v for verbose output, add -f for additional files to be merged +``` + +This command will: + +1. If there are changes to existing processes in the updated `process-compose.yaml` file, stop the old instances of these processes and start new instances with the updated config. +2. If there are only new processes in the updated `process-compose.yaml` file, start the new processes without affecting the others. +3. If some processes no longer exist in the updated `process-compose.yaml` file, stop only those old processes without touching the others. + +### Process Edit + +To edit a single process: + +1. Select it in the TUI or in the TUI client. +2. Press `CTRL+E` +3. Apply the changes, save and quit the editor. +4. The process will restart with the new configuration, or won't restart if there are no changes. + +:bulb: **Notes:** + +1. These changes are not persisted and not applied to your `process-compose.yaml` +2. In case of parsing errors or unrecognized fields: + 1. All the changes will be reverted to the last known correct state. + 2. The editor will open again with a detailed error description at the top of the file. +3. Process Compose will use one of: + 1. Your default editor defined in `$EDITOR` environment variable. If empty: + 2. For non-Windows OSs: `vim`, `nano`, `vi` in that order. + 3. For Windows OS: `notepad.exe`, `notepad++.exe`, `code.exe`, `gvim.exe` in that order. +4. Some of the fields are read only. + ## Backend For cases where your process compose requires a non default or transferable backend definition, setting an environment variable won't do. For that, you can configure it directly in the `process-compose.yaml` file: diff --git a/www/docs/index.md b/www/docs/index.md index 86ef5a9e..ebae695c 100644 --- a/www/docs/index.md +++ b/www/docs/index.md @@ -44,3 +44,5 @@ Check the [Documentation](launcher.md) for more advanced use cases. - Run Multiple Replicas of a Process - Run a Foreground Process - Themes Support +- On the fly Process configuration edit +- On the fly Project update diff --git a/www/docs/tui.md b/www/docs/tui.md index 060a366f..1ad248ab 100644 --- a/www/docs/tui.md +++ b/www/docs/tui.md @@ -7,6 +7,7 @@ TUI Allows you to: - Stop processes - Review logs - Restart running processes +- Edit processes' configuration TUI is the default run mode, but it's possible to disable it: