Skip to content

Commit

Permalink
feat: Added ability to edit processes on the fly
Browse files Browse the repository at this point in the history
  • Loading branch information
F1bonacc1 committed Sep 23, 2024
1 parent c3f3293 commit 3f53128
Show file tree
Hide file tree
Showing 23 changed files with 441 additions and 62 deletions.
21 changes: 21 additions & 0 deletions src/api/pc_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/app/project_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
74 changes: 65 additions & 9 deletions src/app/project_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
}
}
29 changes: 20 additions & 9 deletions src/app/system_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -660,7 +660,7 @@ func TestUpdateProject(t *testing.T) {
"VAR2=value2",
},
},
"process2": {
proc2: {
Name: proc2,
ReplicaName: proc2,
Executable: shell.ShellCommand,
Expand All @@ -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,
Expand All @@ -694,7 +694,7 @@ func TestUpdateProject(t *testing.T) {
"VAR2=value2",
},
},
"process2": {
proc2: {
Name: proc2,
ReplicaName: proc2,
Executable: shell.ShellCommand,
Expand All @@ -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,
Expand All @@ -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",
Expand All @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
26 changes: 26 additions & 0 deletions src/client/processes.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand Down Expand Up @@ -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)
}
4 changes: 3 additions & 1 deletion src/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"context"
"errors"
"fmt"
"github.com/f1bonacc1/process-compose/src/admitter"
"github.com/f1bonacc1/process-compose/src/api"
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions src/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions src/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions src/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 3f53128

Please sign in to comment.