diff --git a/README.md b/README.md index 25a5220..8894380 100755 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Process Compose is a simple and flexible scheduler and orchestrator to manage no - Forking (services or daemons) processes - REST API (OpenAPI a.k.a Swagger) - Logs caching +- Functions as both server and client It is heavily inspired by [docker-compose](https://github.com/docker/compose), but without the need for containers. The configuration syntax tries to follow the docker-compose specifications, with a few minor additions and lots of subtractions. @@ -394,10 +395,54 @@ Default environment variables: #### ✅ REST API -A convenient Swagger API is provided: http://localhost:8080/swagger/index.html +A convenient Swagger API is provided: http://localhost:8080 Swagger +Default port is 8080. Specify your own port: + +```shell +process-compose -p PORT +``` + +#### ✅ Client Mode + +Process compose can also connect to itself as client. Available commands: + +##### Processes List + +```shell +process-compose process list #lists available processes +``` + +##### Process Start + +```shell +process-compose process start [PROCESS] #starts one of the available non running processes +``` + +##### Process Stop + +```shell +process-compose process stop [PROCESS] #stops one of the running processes +``` + +##### Process Restart + +```shell +process-compose process start [PROCESS] #restarts one of the available processes +``` + +Restart will wait `process.availability.backoff_seconds` seconds between `stop` and `start` of the process. If not configured the default value is 1s. + +By default the client will try to use the default port `8080` and default address `localhost` to connect to the locally running instance of process-compose. You can provide deferent values: + +```shell +process-compose -p PORT process -a ADDRESS list +``` + + + #### ✅ Configuration ##### ✅ Support .env file diff --git a/default.nix b/default.nix index c059917..ebf309a 100644 --- a/default.nix +++ b/default.nix @@ -6,7 +6,7 @@ pkgs.buildGoModule rec { src = ./.; ldflags = [ "-X main.version=v${version}" ]; - vendorSha256 = "bJXym+JTAIbEh/dIkY22HdNwgYbdCGgUIts0KkS0GYk="; + vendorSha256 = "RqPH8gm8K8sLuRl4FTpGeitS++t3ygAgZ6OMvBCsCB8="; postInstall = "mv $out/bin/{src,process-compose}"; diff --git a/go.mod b/go.mod index ce507f4..f797eca 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/gin-gonic/gin v1.8.1 github.com/joho/godotenv v1.4.0 github.com/rivo/tview v0.0.0-20220916081518-2e69b7385a37 - github.com/swaggo/swag v1.8.6 + github.com/swaggo/swag v1.8.7 gopkg.in/yaml.v2 v2.4.0 ) @@ -18,7 +18,9 @@ replace github.com/InVisionApp/go-health/v2 => github.com/f1bonacc1/go-health/v2 require ( github.com/InVisionApp/go-logger v1.0.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/gdamore/encoding v1.0.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect @@ -28,6 +30,7 @@ require ( github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/goccy/go-json v0.9.11 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect @@ -38,7 +41,11 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/rivo/uniseg v0.4.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/cobra v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/ugorji/go/codec v1.2.7 // indirect + github.com/urfave/cli/v2 v2.3.0 // indirect golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect golang.org/x/net v0.0.0-20220926192436-02166a98028e // indirect golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect diff --git a/go.sum b/go.sum index 9d4061f..a4b8eea 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -32,6 +34,7 @@ github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= github.com/gdamore/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0= github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= @@ -77,6 +80,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/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -150,12 +155,18 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= +github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -174,10 +185,13 @@ github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89 github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.8.6 h1:2rgOaLbonWu1PLP6G+/rYjSvPg0jQE0HtrEKuE380eg= github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= +github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU= +github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/gopher-lua v0.0.0-20190514113301-1cd887cd7036/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= diff --git a/src/api/pc_api.go b/src/api/pc_api.go index 3dd315c..a9396d6 100644 --- a/src/api/pc_api.go +++ b/src/api/pc_api.go @@ -97,3 +97,22 @@ func StartProcess(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"name": name}) } + +// @Schemes +// @Description Restarts the process +// @Tags Process +// @Summary Restart a process +// @Produce json +// @Param name path string true "Process Name" +// @Success 200 {string} string "Restarted Process Name" +// @Router /process/restart/{name} [post] +func RestartProcess(c *gin.Context) { + name := c.Param("name") + err := app.PROJ.RestartProcess(name) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"name": name}) +} diff --git a/src/api/routes.go b/src/api/routes.go index 0a6a205..f1d3c05 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -5,6 +5,8 @@ import ( "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" + "net/http" + "net/url" ) // @title Process Compose API @@ -32,10 +34,16 @@ func InitRoutes(useLogger bool) *gin.Engine { //url := ginSwagger.URL("http://localhost:8080/swagger/doc.json") // The url pointing to API definition r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + r.GET("/", func(c *gin.Context) { + location := url.URL{Path: "/swagger/index.html"} + c.Redirect(http.StatusFound, location.RequestURI()) + }) + r.GET("/processes", GetProcesses) r.GET("/process/logs/:name/:endOffset/:limit", GetProcessLogs) r.PATCH("/process/stop/:name", StopProcess) r.POST("/process/start/:name", StartProcess) + r.POST("/process/restart/:name", RestartProcess) return r } diff --git a/src/app/config.go b/src/app/config.go index e8e7ad0..2a60142 100644 --- a/src/app/config.go +++ b/src/app/config.go @@ -49,6 +49,10 @@ type ProcessState struct { Pid int `json:"pid"` } +type ProcessStates struct { + States []ProcessState `json:"data"` +} + func (p ProcessConfig) GetDependencies() []string { dependencies := make([]string, len(p.DependsOn)) diff --git a/src/app/process.go b/src/app/process.go index 7e72943..6d2cd58 100644 --- a/src/app/process.go +++ b/src/app/process.go @@ -13,7 +13,7 @@ import ( "syscall" "time" - "github.com/f1bonacc1/process-compose/src/cmd" + "github.com/f1bonacc1/process-compose/src/command" "github.com/f1bonacc1/process-compose/src/health" "github.com/f1bonacc1/process-compose/src/pclog" @@ -85,7 +85,7 @@ func (p *Process) run() error { } for { starter := func() error { - p.command = cmd.BuildCommand(p.getCommand()) + p.command = command.BuildCommand(p.getCommand()) p.command.Env = p.getProcessEnvironment() p.setProcArgs() stdout, _ := p.command.StdoutPipe() @@ -116,7 +116,7 @@ func (p *Process) run() error { p.waitForDaemonCompletion() } - if !p.isRestartable(p.procState.ExitCode) { + if !p.isRestartable() { break } p.setState(ProcessStateRestarting) @@ -149,7 +149,8 @@ func (p *Process) getProcessEnvironment() []string { return env } -func (p *Process) isRestartable(exitCode int) bool { +func (p *Process) isRestartable() bool { + exitCode := p.procState.ExitCode if p.procConf.RestartPolicy.Restart == RestartPolicyNo || p.procConf.RestartPolicy.Restart == "" { return false @@ -223,10 +224,10 @@ func (p *Process) doConfiguredStop(params ShutDownParams) error { defer cancel() defer p.notifyDaemonStopped() - command := cmd.BuildCommandContext(ctx, params.ShutDownCommand) - command.Env = p.getProcessEnvironment() + cmd := command.BuildCommandContext(ctx, params.ShutDownCommand) + cmd.Env = p.getProcessEnvironment() - if err := command.Run(); err != nil { + if err := cmd.Run(); err != nil { // the process termination timedout and it will be killed log.Error().Msgf("terminating %s with timeout %d failed - %s", p.getName(), timeout, err.Error()) return p.stop(int(syscall.SIGKILL)) @@ -393,7 +394,7 @@ func (p *Process) stopProbes() { } } -func (p *Process) onLivenessCheckEnd(isOk, isFatal bool, err string) { +func (p *Process) onLivenessCheckEnd(_, isFatal bool, err string) { if isFatal { log.Info().Msgf("%s is not alive anymore - %s", p.getName(), err) p.logBuffer.Write("Error: liveness check fail - " + err) diff --git a/src/app/project.go b/src/app/project.go index e4d8251..39e2488 100644 --- a/src/app/project.go +++ b/src/app/project.go @@ -3,13 +3,12 @@ package app import ( "errors" "fmt" - "io/ioutil" + "github.com/f1bonacc1/process-compose/src/pclog" "os" "path/filepath" "sort" "strings" - - "github.com/f1bonacc1/process-compose/src/pclog" + "time" "github.com/joho/godotenv" "github.com/rs/zerolog" @@ -170,9 +169,9 @@ func (p *Project) StartProcess(name string) error { log.Error().Msgf("Process %s is already running", name) return fmt.Errorf("process %s is already running", name) } - if proc, ok := p.Processes[name]; ok { - proc.Name = name - p.runProcess(proc) + if processConfig, ok := p.Processes[name]; ok { + processConfig.Name = name + p.runProcess(processConfig) } else { return fmt.Errorf("no such process: %s", name) } @@ -190,6 +189,25 @@ func (p *Project) StopProcess(name string) error { 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) ShutDownProject() { p.mapMutex.Lock() runProc := p.runningProcesses @@ -331,7 +349,7 @@ func (p *Project) GetLexicographicProcessNames() []string { } func CreateProject(inputFile string) *Project { - yamlFile, err := ioutil.ReadFile(inputFile) + yamlFile, err := os.ReadFile(inputFile) if err != nil { if errors.Is(err, os.ErrNotExist) { diff --git a/src/client/common.go b/src/client/common.go new file mode 100644 index 0000000..4d7c972 --- /dev/null +++ b/src/client/common.go @@ -0,0 +1,5 @@ +package client + +type pcError struct { + Error string `json:"error"` +} diff --git a/src/client/processes.go b/src/client/processes.go new file mode 100644 index 0000000..42b1f95 --- /dev/null +++ b/src/client/processes.go @@ -0,0 +1,29 @@ +package client + +import ( + "encoding/json" + "fmt" + "github.com/f1bonacc1/process-compose/src/app" + "net/http" +) + +func GetProcessesName(address string, port int) ([]string, error) { + url := fmt.Sprintf("http://%s:%d/processes", address, port) + resp, err := http.Get(url) + if err != nil { + return []string{}, err + } + defer resp.Body.Close() + //Create a variable of the same type as our model + var sResp app.ProcessStates + + //Decode the data + if err := json.NewDecoder(resp.Body).Decode(&sResp); err != nil { + return []string{}, err + } + procs := make([]string, len(sResp.States)) + for i, proc := range sResp.States { + procs[i] = proc.Name + } + return procs, nil +} diff --git a/src/client/restart.go b/src/client/restart.go new file mode 100644 index 0000000..b7d1a6a --- /dev/null +++ b/src/client/restart.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + "fmt" + "github.com/rs/zerolog/log" + "net/http" +) + +func RestartProcesses(address string, port int, name string) error { + url := fmt.Sprintf("http://%s:%d/process/restart/%s", address, port, name) + resp, err := http.Post(url, "application/json", nil) + if err != nil { + return err + } + if resp.StatusCode == http.StatusOK { + return nil + } + defer resp.Body.Close() + var respErr pcError + if err = json.NewDecoder(resp.Body).Decode(&respErr); err != nil { + log.Error().Msgf("failed to decode restart process %s response: %v", name, err) + return err + } + return fmt.Errorf(respErr.Error) +} diff --git a/src/client/start.go b/src/client/start.go new file mode 100644 index 0000000..a83ac9e --- /dev/null +++ b/src/client/start.go @@ -0,0 +1,26 @@ +package client + +import ( + "encoding/json" + "fmt" + "github.com/rs/zerolog/log" + "net/http" +) + +func StartProcesses(address string, port int, name string) error { + url := fmt.Sprintf("http://%s:%d/process/start/%s", address, port, name) + resp, err := http.Post(url, "application/json", nil) + if err != nil { + return err + } + if resp.StatusCode == http.StatusOK { + return nil + } + defer resp.Body.Close() + var respErr pcError + if err = json.NewDecoder(resp.Body).Decode(&respErr); err != nil { + log.Error().Msgf("failed to decode start process %s response: %v", name, err) + return err + } + return fmt.Errorf(respErr.Error) +} diff --git a/src/client/stop.go b/src/client/stop.go new file mode 100644 index 0000000..037d4df --- /dev/null +++ b/src/client/stop.go @@ -0,0 +1,31 @@ +package client + +import ( + "encoding/json" + "fmt" + "github.com/rs/zerolog/log" + "net/http" +) + +func StopProcesses(address string, port int, name string) error { + url := fmt.Sprintf("http://%s:%d/process/stop/%s", address, port, name) + client := &http.Client{} + req, err := http.NewRequest(http.MethodPatch, url, nil) + if err != nil { + return err + } + resp, err := client.Do(req) + if err != nil { + return err + } + if resp.StatusCode == http.StatusOK { + return nil + } + defer resp.Body.Close() + var respErr pcError + if err = json.NewDecoder(resp.Body).Decode(&respErr); err != nil { + log.Error().Msgf("failed to decode stop process %s response: %v", name, err) + return err + } + return fmt.Errorf(respErr.Error) +} diff --git a/src/cmd/list.go b/src/cmd/list.go new file mode 100644 index 0000000..6aae7e5 --- /dev/null +++ b/src/cmd/list.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "fmt" + "github.com/f1bonacc1/process-compose/src/client" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +// listCmd represents the list command +var listCmd = &cobra.Command{ + Use: "list", + Short: "List available processes", + Aliases: []string{"ls"}, + Run: func(cmd *cobra.Command, args []string) { + processNames, err := client.GetProcessesName(pcAddress, port) + if err != nil { + log.Error().Msgf("Failed to get processes names %v", err) + return + } + for _, proc := range processNames { + fmt.Println(proc) + } + }, +} + +func init() { + processCmd.AddCommand(listCmd) +} diff --git a/src/cmd/process.go b/src/cmd/process.go new file mode 100644 index 0000000..50025b7 --- /dev/null +++ b/src/cmd/process.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var ( + pcAddress string +) + +// processCmd represents the process command +var processCmd = &cobra.Command{ + Use: "process", + Short: "Execute operations on available processes", + Args: cobra.MinimumNArgs(1), +} + +func init() { + rootCmd.AddCommand(processCmd) + processCmd.PersistentFlags().StringVarP(&pcAddress, "address", "a", "localhost", "address of a running process compose server") +} diff --git a/src/cmd/restart.go b/src/cmd/restart.go new file mode 100644 index 0000000..ae1b8a2 --- /dev/null +++ b/src/cmd/restart.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "github.com/f1bonacc1/process-compose/src/client" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +// restartCmd represents the restart command +var restartCmd = &cobra.Command{ + Use: "restart [PROCESS]", + Short: "Restart a process", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + err := client.RestartProcesses(pcAddress, port, name) + if err != nil { + log.Error().Msgf("Failed to restart processes %s: %v", name, err) + return + } + log.Info().Msgf("Process %s restarted", name) + }, +} + +func init() { + processCmd.AddCommand(restartCmd) +} diff --git a/src/cmd/root.go b/src/cmd/root.go new file mode 100644 index 0000000..a60c3a6 --- /dev/null +++ b/src/cmd/root.go @@ -0,0 +1,126 @@ +package cmd + +import ( + "fmt" + "github.com/f1bonacc1/process-compose/src/api" + "github.com/f1bonacc1/process-compose/src/app" + "github.com/f1bonacc1/process-compose/src/tui" + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +const EnvDebugMode = "PC_DEBUG_MODE" + +var ( + fileName string + port int + isTui bool + version string + + // rootCmd represents the base command when called without any subcommands + rootCmd = &cobra.Command{ + Use: "process-compose", + Short: "Processes scheduler and orchestrator", + // Uncomment the following line if your bare application + // has an action associated with it: + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(fileName) + if !cmd.Flags().Changed("config") { + + pwd, err := os.Getwd() + if err != nil { + log.Fatal().Msg(err.Error()) + } + file, err := app.AutoDiscoverComposeFile(pwd) + if err != nil { + log.Fatal().Msg(err.Error()) + } + fileName = file + } + + if os.Getenv(EnvDebugMode) == "" { + gin.SetMode(gin.ReleaseMode) + } + + routersInit := api.InitRoutes(!isTui) + readTimeout := time.Duration(60) * time.Second + writeTimeout := time.Duration(60) * time.Second + endPoint := fmt.Sprintf(":%d", port) + maxHeaderBytes := 1 << 20 + + server := &http.Server{ + Addr: endPoint, + Handler: routersInit, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + MaxHeaderBytes: maxHeaderBytes, + } + + log.Info().Msgf("start http server listening %s", endPoint) + + go server.ListenAndServe() + + project := app.CreateProject(fileName) + + if isTui { + defer quiet()() + go project.Run() + tui.SetupTui(version, project.LogLength) + } else { + runHeadless(project) + } + + log.Info().Msg("Thank you for using proccess-compose") + }, + } +) + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute(ver string) { + version = ver + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + + rootCmd.Flags().StringVarP(&fileName, "config", "f", app.DefaultFileNames[0], "path to config file to load") + rootCmd.Flags().BoolVarP(&isTui, "tui", "t", true, "disable tui (-t=false)") + rootCmd.PersistentFlags().IntVarP(&port, "port", "p", 8080, "port number") +} + +func runHeadless(project *app.Project) { + cancelChan := make(chan os.Signal, 1) + // catch SIGTERM or SIGINTERRUPT + signal.Notify(cancelChan, syscall.SIGTERM, syscall.SIGINT) + go project.Run() + sig := <-cancelChan + log.Info().Msgf("Caught %v - Shutting down the running processes...", sig) + project.ShutDownProject() +} + +func quiet() func() { + null, _ := os.Open(os.DevNull) + sout := os.Stdout + serr := os.Stderr + os.Stdout = null + os.Stderr = null + zerolog.SetGlobalLevel(zerolog.Disabled) + return func() { + defer null.Close() + os.Stdout = sout + os.Stderr = serr + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } +} diff --git a/src/cmd/start.go b/src/cmd/start.go new file mode 100644 index 0000000..6ca2292 --- /dev/null +++ b/src/cmd/start.go @@ -0,0 +1,31 @@ +/* +Copyright © 2022 NAME HERE +*/ +package cmd + +import ( + "github.com/f1bonacc1/process-compose/src/client" + "github.com/rs/zerolog/log" + + "github.com/spf13/cobra" +) + +// startCmd represents the start command +var startCmd = &cobra.Command{ + Use: "start [PROCESS]", + Short: "Start a process", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + err := client.StartProcesses(pcAddress, port, name) + if err != nil { + log.Error().Msgf("Failed to start processes %s: %v", name, err) + return + } + log.Info().Msgf("Process %s started", name) + }, +} + +func init() { + processCmd.AddCommand(startCmd) +} diff --git a/src/cmd/stop.go b/src/cmd/stop.go new file mode 100644 index 0000000..87c1740 --- /dev/null +++ b/src/cmd/stop.go @@ -0,0 +1,30 @@ +/* +Copyright © 2022 NAME HERE +*/ +package cmd + +import ( + "github.com/f1bonacc1/process-compose/src/client" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +// stopCmd represents the stop command +var stopCmd = &cobra.Command{ + Use: "stop [PROCESS]", + Short: "Stop a running process", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + err := client.StopProcesses(pcAddress, port, name) + if err != nil { + log.Error().Msgf("Failed to stop processes %s: %v", name, err) + return + } + log.Info().Msgf("Process %s stopped", name) + }, +} + +func init() { + processCmd.AddCommand(stopCmd) +} diff --git a/src/cmd/command.go b/src/command/command.go similarity index 97% rename from src/cmd/command.go rename to src/command/command.go index 682419d..d0c63ce 100644 --- a/src/cmd/command.go +++ b/src/command/command.go @@ -1,4 +1,4 @@ -package cmd +package command import ( "context" diff --git a/src/docs/docs.go b/src/docs/docs.go index 61d83d5..5c6c494 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -59,6 +59,35 @@ const docTemplate = `{ } } }, + "/process/restart/{name}": { + "post": { + "description": "Restarts the process", + "produces": [ + "application/json" + ], + "tags": [ + "Process" + ], + "summary": "Restart a process", + "parameters": [ + { + "type": "string", + "description": "Process Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Restarted Process Name", + "schema": { + "type": "string" + } + } + } + } + }, "/process/start/{name}": { "post": { "description": "Starts the process if the state is not 'running' or 'pending'", diff --git a/src/docs/swagger.json b/src/docs/swagger.json index d401d31..6425873 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -47,6 +47,35 @@ } } }, + "/process/restart/{name}": { + "post": { + "description": "Restarts the process", + "produces": [ + "application/json" + ], + "tags": [ + "Process" + ], + "summary": "Restart a process", + "parameters": [ + { + "type": "string", + "description": "Process Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Restarted Process Name", + "schema": { + "type": "string" + } + } + } + } + }, "/process/start/{name}": { "post": { "description": "Starts the process if the state is not 'running' or 'pending'", diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 3d5e335..eef1759 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -30,6 +30,25 @@ paths: summary: Get process logs tags: - Process + /process/restart/{name}: + post: + description: Restarts the process + parameters: + - description: Process Name + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: Restarted Process Name + schema: + type: string + summary: Restart a process + tags: + - Process /process/start/{name}: post: description: Starts the process if the state is not 'running' or 'pending' diff --git a/src/health/exec_checker.go b/src/health/exec_checker.go index 46eed79..9ead695 100644 --- a/src/health/exec_checker.go +++ b/src/health/exec_checker.go @@ -2,9 +2,8 @@ package health import ( "context" + "github.com/f1bonacc1/process-compose/src/command" "time" - - "github.com/f1bonacc1/process-compose/src/cmd" ) type execChecker struct { @@ -16,7 +15,7 @@ func (c *execChecker) Status() (interface{}, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.timeout)*time.Second) defer cancel() - cmd := cmd.BuildCommandContext(ctx, c.command) + cmd := command.BuildCommandContext(ctx, c.command) if err := cmd.Run(); err != nil { return nil, err diff --git a/src/main.go b/src/main.go index b733986..78108ff 100644 --- a/src/main.go +++ b/src/main.go @@ -1,25 +1,12 @@ package main import ( - "flag" - "fmt" - "net/http" - "os/signal" - "syscall" - "time" - - "os" - - "github.com/f1bonacc1/process-compose/src/api" - "github.com/f1bonacc1/process-compose/src/app" - "github.com/f1bonacc1/process-compose/src/tui" - "github.com/gin-gonic/gin" + "github.com/f1bonacc1/process-compose/src/cmd" "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "os" ) -const EnvDebugMode = "PC_DEBUG_MODE" - var version = "undefined" func setupLogger() { @@ -31,99 +18,10 @@ func setupLogger() { zerolog.SetGlobalLevel(zerolog.DebugLevel) } -func isFlagPassed(name string) bool { - found := false - flag.Visit(func(f *flag.Flag) { - if f.Name == name { - found = true - } - }) - return found -} - func init() { setupLogger() } -func quiet() func() { - null, _ := os.Open(os.DevNull) - sout := os.Stdout - serr := os.Stderr - os.Stdout = null - os.Stderr = null - zerolog.SetGlobalLevel(zerolog.Disabled) - return func() { - defer null.Close() - os.Stdout = sout - os.Stderr = serr - zerolog.SetGlobalLevel(zerolog.DebugLevel) - } -} - -func runHeadless(project *app.Project) { - cancelChan := make(chan os.Signal, 1) - // catch SIGTERM or SIGINTERRUPT - signal.Notify(cancelChan, syscall.SIGTERM, syscall.SIGINT) - go func() { - project.Run() - }() - sig := <-cancelChan - log.Info().Msgf("Caught %v - Shutting down the running processes...", sig) - project.ShutDownProject() -} - func main() { - fileName := "" - port := 8080 - isTui := true - flag.StringVar(&fileName, "f", app.DefaultFileNames[0], "path to file to load") - flag.IntVar(&port, "p", port, "port number") - flag.BoolVar(&isTui, "t", isTui, "disable tui (-t=false)") - flag.Parse() - if !isFlagPassed("f") { - pwd, err := os.Getwd() - if err != nil { - log.Fatal().Msg(err.Error()) - } - file, err := app.AutoDiscoverComposeFile(pwd) - if err != nil { - log.Fatal().Msg(err.Error()) - } - fileName = file - } - - if os.Getenv(EnvDebugMode) == "" { - gin.SetMode(gin.ReleaseMode) - } - - routersInit := api.InitRoutes(!isTui) - readTimeout := time.Duration(60) * time.Second - writeTimeout := time.Duration(60) * time.Second - endPoint := fmt.Sprintf(":%d", port) - maxHeaderBytes := 1 << 20 - - server := &http.Server{ - Addr: endPoint, - Handler: routersInit, - ReadTimeout: readTimeout, - WriteTimeout: writeTimeout, - MaxHeaderBytes: maxHeaderBytes, - } - - log.Info().Msgf("start http server listening %s", endPoint) - - go server.ListenAndServe() - - project := app.CreateProject(fileName) - - if isTui { - defer quiet()() - go project.Run() - tui.SetupTui(version, project.LogLength) - } else { - runHeadless(project) - } - - log.Info().Msg("Thank you for using proccess-compose") - + cmd.Execute(version) } diff --git a/src/tui/view.go b/src/tui/view.go index af8641f..831498c 100644 --- a/src/tui/view.go +++ b/src/tui/view.go @@ -216,6 +216,9 @@ func (pv *pcView) createProcTable() *tview.Table { case tcell.KeyF7: name := pv.getSelectedProcName() app.PROJ.StartProcess(name) + case tcell.KeyCtrlR: + name := pv.getSelectedProcName() + app.PROJ.RestartProcess(name) } return event }) @@ -294,6 +297,7 @@ func (pv *pcView) updateHelpTextView() { fmt.Fprintf(pv.helpText, "%s ", "F7[black:green]Start[-:-:-]") fmt.Fprintf(pv.helpText, "%s%s%s ", "F8[black:green]", procScr, " Screen[-:-:-]") fmt.Fprintf(pv.helpText, "%s ", "F9[black:green]Kill[-:-:-]") + fmt.Fprintf(pv.helpText, "%s ", "CTRL+R[black:green]Restart[-:-:-]") fmt.Fprintf(pv.helpText, "%s ", "F10[black:green]Quit[-:-:-]") }