Skip to content

Commit

Permalink
feat #233: process-compose update
Browse files Browse the repository at this point in the history
  • Loading branch information
F1bonacc1 committed Sep 13, 2024
1 parent a4ee292 commit b230d41
Show file tree
Hide file tree
Showing 44 changed files with 706 additions and 54 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ coverhtml:
go tool cover -html=coverage.out

run:
PC_DEBUG_MODE=1 ./bin/${NAME}${EXT} -e .env -e .env.local
PC_DEBUG_MODE=1 ./bin/${NAME}${EXT} -e .env

clean:
$(RM) bin/${NAME}*
Expand Down
30 changes: 28 additions & 2 deletions src/api/pc_api.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"github.com/f1bonacc1/process-compose/src/types"
"net/http"
"strconv"

Expand Down Expand Up @@ -275,6 +276,31 @@ func (api *PcApi) ShutDownProject(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "stopped"})
}

// @Schemes
// @Description Update running project
// @Tags Project
// @Summary Updates running processes
// @Produce json
// @Success 200
// @Router /project [post]
func (api *PcApi) UpdateProject(c *gin.Context) {
var project types.Project
if err := c.ShouldBindJSON(&project); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
status, err := api.project.UpdateProject(&project)
if err != nil {
if len(status) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
} else {
c.JSON(http.StatusMultiStatus, status)
}
return
}
c.JSON(http.StatusOK, status)
}

// @Schemes
// @Description Retrieves project state information
// @Tags Project
Expand All @@ -285,12 +311,12 @@ func (api *PcApi) ShutDownProject(c *gin.Context) {
func (api *PcApi) GetProjectState(c *gin.Context) {
withMemory := c.DefaultQuery("withMemory", "false")
checkMem, _ := strconv.ParseBool(withMemory)
ports, err := api.project.GetProjectState(checkMem)
state, err := api.project.GetProjectState(checkMem)

if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

c.JSON(http.StatusOK, ports)
c.JSON(http.StatusOK, state)
}
1 change: 1 addition & 0 deletions src/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func InitRoutes(useLogger bool, handler *PcApi) *gin.Engine {
r.POST("/process/start/:name", handler.StartProcess)
r.POST("/process/restart/:name", handler.RestartProcess)
r.POST("/project/stop", handler.ShutDownProject)
r.POST("/project", handler.UpdateProject)
r.GET("/project/state", handler.GetProjectState)
r.PATCH("/process/scale/:name/:scale", handler.ScaleProcess)

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 @@ -29,4 +29,5 @@ type IProject interface {
ScaleProcess(name string, scale int) error
GetProcessPorts(name string) (*types.ProcessPorts, error)
SetProcessPassword(name string, password string) error
UpdateProject(project *types.Project) (map[string]string, error)
}
67 changes: 57 additions & 10 deletions src/app/project_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app

import (
"context"
"errors"
"fmt"
"github.com/f1bonacc1/process-compose/src/config"
"github.com/f1bonacc1/process-compose/src/pclog"
Expand Down Expand Up @@ -842,13 +843,59 @@ func NewProjectRunner(opts *ProjectOpts) (*ProjectRunner, error) {
return runner, nil
}

//func getProcessName(process *Process) string {
// return process.getNameWithSmartReplica()
//}
//
//func getProcessNameFromConf(process types.ProcessConfig, replica int) string {
// if process.Replicas > 1 {
// return fmt.Sprintf("%s-%d", process.Name, replica)
// }
// return process.Name
//}
func (p *ProjectRunner) UpdateProject(project *types.Project) (map[string]string, error) {
newProcs := make(map[string]types.ProcessConfig)
delProcs := make(map[string]types.ProcessConfig)
updatedProcs := make(map[string]types.ProcessConfig)
for name, newProc := range project.Processes {
if currentProc, ok := p.project.Processes[name]; ok {
equal := currentProc.Compare(&newProc)
if equal {
log.Debug().Msgf("Process %s is up to date", name)
continue
}
log.Debug().Msgf("Process %s is updated", name)
updatedProcs[name] = newProc
} else {
log.Debug().Msgf("Process %s is new", name)
newProcs[name] = newProc
}
}
for name, currentProc := range p.project.Processes {
if _, ok := project.Processes[name]; !ok {
log.Debug().Msgf("Process %s is deleted", name)
delProcs[name] = currentProc
}
}
status := make(map[string]string)
errs := make([]error, 0)
//Delete removed processes
for name := range delProcs {
err := p.removeProcess(name)
if err != nil {
log.Err(err).Msgf("Failed to remove process %s", name)
errs = append(errs, err)
status[name] = types.ProcessUpdateError
continue
}
status[name] = types.ProcessUpdateRemoved
}
//Add new processes
for name, proc := range newProcs {
p.addProcessAndRun(proc)
status[name] = types.ProcessUpdateAdded
}
//Update processes
for name, proc := range updatedProcs {
err := p.removeProcess(name)
if err != nil {
log.Err(err).Msgf("Failed to remove process %s", name)
errs = append(errs, err)
status[name] = types.ProcessUpdateError
continue
}
p.addProcessAndRun(proc)
status[name] = types.ProcessUpdateUpdated
}
return status, errors.Join(errs...)
}
170 changes: 170 additions & 0 deletions src/app/system_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -639,3 +639,173 @@ func TestSystem_TestReadyLine(t *testing.T) {
return
}
}

func TestUpdateProject(t *testing.T) {
proc1 := "process1"
proc2 := "process2"
proc3 := "process3"
shell := command.DefaultShellConfig()
p, err := NewProjectRunner(&ProjectOpts{
project: &types.Project{
ShellConfig: shell,
Processes: map[string]types.ProcessConfig{
"process1": {
Name: proc1,
ReplicaName: proc1,
Executable: shell.ShellCommand,
Args: []string{shell.ShellArgument, "echo process1"},
Environment: []string{
"VAR1=value1",
"VAR2=value2",
},
},
"process2": {
Name: proc2,
ReplicaName: proc2,
Executable: shell.ShellCommand,
Args: []string{shell.ShellArgument, "echo process2"},
Environment: []string{
"VAR3=value3",
"VAR4=value4",
},
},
},
},
})
if err != nil {
t.Errorf(err.Error())
return
}
go p.Run()
time.Sleep(100 * time.Millisecond)

// Test when no changes are made
project := &types.Project{
ShellConfig: shell,
Processes: map[string]types.ProcessConfig{
"process1": {
Name: proc1,
ReplicaName: proc1,
Executable: shell.ShellCommand,
Args: []string{shell.ShellArgument, "echo process1"},
Environment: []string{
"VAR1=value1",
"VAR2=value2",
},
},
"process2": {
Name: proc2,
ReplicaName: proc2,
Executable: shell.ShellCommand,
Args: []string{shell.ShellArgument, "echo process2"},
Environment: []string{
"VAR3=value3",
"VAR4=value4",
},
},
},
}
status, err := p.UpdateProject(project)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if len(status) != 0 {
t.Errorf("Unexpected status: %v", status)
}

// Test when a process is updated
project = &types.Project{
ShellConfig: shell,
Processes: map[string]types.ProcessConfig{
"process1": {
Name: proc1,
ReplicaName: proc1,
Executable: shell.ShellCommand,
Args: []string{shell.ShellArgument, "echo updated"},
Environment: []string{
"VAR1=value1",
"VAR2=value2",
},
},
"process2": {
Name: proc2,
ReplicaName: proc2,
Executable: shell.ShellCommand,
Args: []string{shell.ShellArgument, "echo process2"},
Environment: []string{
"VAR3=value3",
"VAR4=value4",
},
},
},
}
status, err = p.UpdateProject(project)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
proc, ok := p.project.Processes[proc1]
if !ok {
t.Errorf("Process 'process1' not found in updated project")
}
if proc.Args[1] != "echo updated" {
t.Errorf("Process 'process1' command is %s want 'echo updated'", proc.Args[1])
}
updatedStatus := status[proc1]
if updatedStatus != types.ProcessUpdateUpdated {
t.Errorf("Process 'process1' status is %s want %s", updatedStatus, types.ProcessUpdateUpdated)
}

// Test when a process is deleted
project = &types.Project{
Processes: map[string]types.ProcessConfig{
"process2": {
Name: proc2,
ReplicaName: proc2,
Executable: shell.ShellCommand,
Args: []string{shell.ShellArgument, "echo process2"},
Environment: []string{
"VAR3=value3",
"VAR4=value4",
},
},
},
}
status, err = p.UpdateProject(project)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if _, ok = p.project.Processes[proc1]; ok {
t.Errorf("Process 'process1' still exists in updated project")
}
updatedStatus = status[proc1]
if updatedStatus != types.ProcessUpdateRemoved {
t.Errorf("Process 'process1' status is %s want %s", updatedStatus, types.ProcessUpdateRemoved)
}

// Test when a new process is added
project = &types.Project{
Processes: map[string]types.ProcessConfig{
"process3": {
Name: proc3,
ReplicaName: proc3,
Executable: shell.ShellCommand,
Args: []string{shell.ShellArgument, "echo process3"},
Environment: []string{
"VAR5=value5",
"VAR6=value6",
},
},
},
}
status, err = p.UpdateProject(project)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if _, ok = p.project.Processes[proc3]; !ok {
t.Errorf("Process 'process3' not found in updated project")
}
updatedStatus = status[proc3]
if updatedStatus != types.ProcessUpdateAdded {
t.Errorf("Process 'process1' status is %s want %s", updatedStatus, types.ProcessUpdateAdded)
}
}
4 changes: 4 additions & 0 deletions src/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,7 @@ func (p *PcClient) GetProjectState(withMemory bool) (*types.ProjectState, error)
func (p *PcClient) SetProcessPassword(_, _ string) error {
return fmt.Errorf("set process password not allowed for PC client")
}

func (p *PcClient) UpdateProject(project *types.Project) (map[string]string, error) {
return p.updateProject(project)
}
34 changes: 33 additions & 1 deletion src/client/project.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package client

import (
"bytes"
"encoding/json"
"fmt"
"github.com/f1bonacc1/process-compose/src/types"
Expand Down Expand Up @@ -40,9 +41,40 @@ func (p *PcClient) getProjectState(withMemory bool) (*types.ProjectState, error)
var sResp types.ProjectState

//Decode the data
if err := json.NewDecoder(resp.Body).Decode(&sResp); err != nil {
if err = json.NewDecoder(resp.Body).Decode(&sResp); err != nil {
log.Err(err).Msgf("failed to decode process states")
return nil, err
}
return &sResp, nil
}

func (p *PcClient) updateProject(project *types.Project) (map[string]string, error) {
url := fmt.Sprintf("http://%s/project", p.address)
jsonData, err := json.Marshal(project)
if err != nil {
log.Err(err).Msg("failed to marshal project")
return nil, err
}
resp, err := p.client.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
log.Err(err).Msg("failed to update project")
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusMultiStatus {
status := map[string]string{}
if err = json.NewDecoder(resp.Body).Decode(&status); err != nil {
log.Err(err).Msg("failed to decode updated processes")
return status, err
}
log.Info().Msgf("status: %v", status)

return status, nil
}
var respErr pcError
if err = json.NewDecoder(resp.Body).Decode(&respErr); err != nil {
log.Err(err).Msg("failed to decode err update project")
return nil, err
}
return nil, fmt.Errorf(respErr.Error)
}
2 changes: 1 addition & 1 deletion src/client/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (p *PcClient) stopProcesses(names []string) (map[string]string, error) {
}
var respErr pcError
if err = json.NewDecoder(resp.Body).Decode(&respErr); err != nil {
log.Err(err).Msgf("failed to decode err stop process %v", names)
log.Err(err).Msgf("failed to decode err stop processes %v", names)
return nil, err
}
return nil, fmt.Errorf(respErr.Error)
Expand Down
Loading

0 comments on commit b230d41

Please sign in to comment.