diff --git a/go.mod b/go.mod index dff64e4..c89fb04 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/InVisionApp/go-health/v2 v2.1.3 github.com/adrg/xdg v0.4.0 + github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 github.com/f1bonacc1/glippy v0.0.0-20230203184233-82c6562cecd1 github.com/fatih/color v1.15.0 github.com/gdamore/tcell/v2 v2.6.0 @@ -16,10 +17,13 @@ require ( github.com/spf13/cobra v1.7.0 github.com/swaggo/swag v1.16.1 gopkg.in/yaml.v2 v2.4.0 +//github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 ) replace github.com/InVisionApp/go-health/v2 => github.com/f1bonacc1/go-health/v2 v2.1.3 +replace github.com/cakturk/go-netstat => github.com/mololab/netstat v0.0.0-20221129160431-27c9226a45b1 + require ( github.com/InVisionApp/go-logger v1.0.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect diff --git a/go.sum b/go.sum index ffc6738..20aad23 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/InVisionApp/go-logger v1.0.1 h1:WFL19PViM1mHUmUWfsv5zMo379KSWj2MRmBlz github.com/InVisionApp/go-logger v1.0.1/go.mod h1:+cGTDSn+P8105aZkeOfIhdd7vFO5X1afUHcjvanY0L8= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= @@ -14,6 +16,8 @@ github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwPhhyuFkbINB+2a1xATwk8SNDWnJiD41g= +github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= @@ -128,6 +132,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mololab/netstat v0.0.0-20221129160431-27c9226a45b1 h1:3k8Fbl7pFlde2/847ox7l/Wqql9Ww644JsQoPaY3/1o= +github.com/mololab/netstat v0.0.0-20221129160431-27c9226a45b1/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/process-compose.yaml b/process-compose.yaml index 7e24807..958440e 100644 --- a/process-compose.yaml +++ b/process-compose.yaml @@ -84,36 +84,8 @@ processes: success_threshold: 1 failure_threshold: 3 -# nginx: -# command: "docker run -d --rm -p80:80 --name nginx_test nginx" -# # availability: -# # restart: on_failure -# is_daemon: true -# shutdown: -# command: "docker stop nginx_test" -# signal: 15 -# timeout_seconds: 5 -# liveness_probe: -# exec: -# command: "[ $(docker inspect -f '{{.State.Running}}' nginx_test) = 'true' ]" -# initial_delay_seconds: 5 -# period_seconds: 2 -# timeout_seconds: 5 -# success_threshold: 1 -# failure_threshold: 3 -# readiness_probe: -# http_get: -# host: 127.0.0.1 -# path: "/" -# port: 80 -# initial_delay_seconds: 5 -# period_seconds: 10 -# timeout_seconds: 5 -# success_threshold: 1 -# failure_threshold: 3 -# availability: -# restart: "always" -# backoff_seconds: 2 + server: + command: "python3 -m http.server 4040" kcalc: command: "kcalc" diff --git a/src/api/pc_api.go b/src/api/pc_api.go index 535a8d1..c648901 100644 --- a/src/api/pc_api.go +++ b/src/api/pc_api.go @@ -215,3 +215,23 @@ func (api *PcApi) GetHostName(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"name": name}) } + +// @Schemes +// @Description Retrieves process open ports +// @Tags Process +// @Summary Get process ports +// @Produce json +// @Param name path string true "Process Name" +// @Success 200 {object} object "Process Ports" +// @Router /process/ports/{name} [get] +func (api *PcApi) GetProcessPorts(c *gin.Context) { + name := c.Param("name") + + ports, err := api.project.GetProcessPorts(name) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, ports) +} diff --git a/src/api/routes.go b/src/api/routes.go index 72eeff8..c6e0108 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.GET("/process/ports/:name", handler.GetProcessPorts) r.GET("/process/logs/:name/:endOffset/:limit", handler.GetProcessLogs) r.PATCH("/process/stop/:name", handler.StopProcess) r.POST("/process/start/:name", handler.StartProcess) diff --git a/src/app/process.go b/src/app/process.go index 6cb1919..af00acb 100644 --- a/src/app/process.go +++ b/src/app/process.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "fmt" + "github.com/cakturk/go-netstat/netstat" "github.com/f1bonacc1/process-compose/src/types" "io" "math/rand" @@ -160,7 +161,7 @@ func (p *Process) getBackoff() time.Duration { func (p *Process) getProcessEnvironment() []string { env := []string{ - "PC_PROC_NAME=" + p.getName(), + "PC_PROC_NAME=" + p.procConf.Name, "PC_REPLICA_NUM=" + strconv.Itoa(p.procConf.ReplicaNum), } env = append(env, os.Environ()...) @@ -528,3 +529,20 @@ func (p *Process) validateProcess() error { } return nil } + +func (p *Process) getOpenPorts(ports *types.ProcessPorts) error { + socks, err := netstat.TCPSocks(func(s *netstat.SockTabEntry) bool { + return s.State == netstat.Listen + }) + if err != nil { + log.Err(err).Msgf("failed to get open ports for %s", p.getName()) + return err + } + for _, e := range socks { + if e.Process != nil && e.Process.Pid == p.procState.Pid { + log.Debug().Msgf("%s is listening on %d", p.getName(), e.LocalAddr.Port) + ports.TcpPorts = append(ports.TcpPorts, e.LocalAddr.Port) + } + } + return nil +} diff --git a/src/app/project_interface.go b/src/app/project_interface.go index b4e2716..5f775a7 100644 --- a/src/app/project_interface.go +++ b/src/app/project_interface.go @@ -25,4 +25,5 @@ type IProject interface { StartProcess(name string) error RestartProcess(name string) error ScaleProcess(name string, scale int) error + GetProcessPorts(name string) (*types.ProcessPorts, error) } diff --git a/src/app/project_runner.go b/src/app/project_runner.go index b1ea9d8..50d7513 100644 --- a/src/app/project_runner.go +++ b/src/app/project_runner.go @@ -260,6 +260,24 @@ func (p *ProjectRunner) GetProcessInfo(name string) (*types.ProcessConfig, error } } +func (p *ProjectRunner) GetProcessPorts(name string) (*types.ProcessPorts, error) { + proc := p.getRunningProcess(name) + if proc == nil { + return nil, fmt.Errorf("can't get ports: process %s is not running", name) + } + + ports := &types.ProcessPorts{ + Name: name, + TcpPorts: make([]uint16, 0), + UdpPorts: make([]uint16, 0), + } + err := proc.getOpenPorts(ports) + if err != nil { + return nil, err + } + return ports, nil +} + func (p *ProjectRunner) ShutDownProject() { p.runProcMutex.Lock() defer p.runProcMutex.Unlock() diff --git a/src/client/client.go b/src/client/client.go index f4fb608..5a72a45 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -72,8 +72,11 @@ func (p *PcClient) GetLexicographicProcessNames() ([]string, error) { } func (p *PcClient) GetProcessInfo(name string) (*types.ProcessConfig, error) { - config, err := GetProcessInfo(p.address, p.port, name) - return config, err + return GetProcessInfo(p.address, p.port, name) +} + +func (p *PcClient) GetProcessPorts(name string) (*types.ProcessPorts, error) { + return GetProcessPorts(p.address, p.port, name) } func (p *PcClient) GetProcessState(name string) (*types.ProcessState, error) { diff --git a/src/client/processes.go b/src/client/processes.go index 091ee99..693f569 100644 --- a/src/client/processes.go +++ b/src/client/processes.go @@ -65,7 +65,6 @@ func GetProcessInfo(address string, port int, name string) (*types.ProcessConfig return nil, err } defer resp.Body.Close() - //Create a variable of the same type as our model var sResp types.ProcessConfig //Decode the data @@ -76,3 +75,21 @@ func GetProcessInfo(address string, port int, name string) (*types.ProcessConfig return &sResp, nil } + +func GetProcessPorts(address string, port int, name string) (*types.ProcessPorts, error) { + url := fmt.Sprintf("http://%s:%d/process/ports/%s", address, port, name) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + var sResp types.ProcessPorts + + //Decode the data + if err := json.NewDecoder(resp.Body).Decode(&sResp); err != nil { + log.Err(err).Msgf("what I got: %s", err.Error()) + return nil, err + } + + return &sResp, nil +} diff --git a/src/cmd/ports.go b/src/cmd/ports.go new file mode 100644 index 0000000..acec76a --- /dev/null +++ b/src/cmd/ports.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" +) + +// portsCmd represents the ports command +var portsCmd = &cobra.Command{ + Use: "ports [PROCESS]", + Short: "Get the ports that a process is listening on", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + ports, err := client.GetProcessPorts(pcAddress, port, name) + if err != nil { + logFatal(err, "failed to get process %s ports", name) + return + } + log.Info().Msgf("Process %s TCP ports: %v", name, ports.TcpPorts) + fmt.Printf("Process %s TCP ports: %v\n", name, ports.TcpPorts) + }, +} + +func init() { + processCmd.AddCommand(portsCmd) +} diff --git a/src/cmd/root.go b/src/cmd/root.go index d341bcd..51fa65e 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "github.com/f1bonacc1/process-compose/src/api" "github.com/f1bonacc1/process-compose/src/loader" "github.com/rs/zerolog/log" @@ -79,3 +80,10 @@ func getConfigDefault() []string { } return []string{} } + +func logFatal(err error, format string, args ...interface{}) { + fmt.Printf(format, args...) + fmt.Printf(": %v\n", err) + log.Err(err).Msgf(format, args...) + os.Exit(1) +} diff --git a/src/docs/docs.go b/src/docs/docs.go index 9c0e2a6..f171857 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -122,6 +122,35 @@ const docTemplate = `{ } } }, + "/process/ports/{name}": { + "get": { + "description": "Retrieves process open ports", + "produces": [ + "application/json" + ], + "tags": [ + "Process" + ], + "summary": "Get process ports", + "parameters": [ + { + "type": "string", + "description": "Process Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Process Ports", + "schema": { + "type": "object" + } + } + } + } + }, "/process/restart/{name}": { "post": { "description": "Restarts the process", diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 5886cad..38ea5b0 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -110,6 +110,35 @@ } } }, + "/process/ports/{name}": { + "get": { + "description": "Retrieves process open ports", + "produces": [ + "application/json" + ], + "tags": [ + "Process" + ], + "summary": "Get process ports", + "parameters": [ + { + "type": "string", + "description": "Process Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Process Ports", + "schema": { + "type": "object" + } + } + } + } + }, "/process/restart/{name}": { "post": { "description": "Restarts the process", diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index f3f4421..0637237 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -90,6 +90,25 @@ paths: summary: Get process logs tags: - Process + /process/ports/{name}: + get: + description: Retrieves process open ports + parameters: + - description: Process Name + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: Process Ports + schema: + type: object + summary: Get process ports + tags: + - Process /process/restart/{name}: post: description: Restarts the process diff --git a/src/tui/proc-info-form.go b/src/tui/proc-info-form.go index 32a7d84..e146f79 100644 --- a/src/tui/proc-info-form.go +++ b/src/tui/proc-info-form.go @@ -8,7 +8,7 @@ import ( "strings" ) -func (pv *pcView) createProcInfoForm(info *types.ProcessConfig) *tview.Form { +func (pv *pcView) createProcInfoForm(info *types.ProcessConfig, ports *types.ProcessPorts) *tview.Form { f := tview.NewForm() f.SetCancelFunc(func() { pv.pages.RemovePage(PageDialog) @@ -23,8 +23,11 @@ func (pv *pcView) createProcInfoForm(info *types.ProcessConfig) *tview.Form { 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) - addSliceIfNotEmpty("Environment:", info.Environment, f) - addSliceIfNotEmpty("Depends On:", mapKeysToSlice(info.DependsOn), f) + addDropDownIfNotEmpty("Environment:", info.Environment, f) + addCSVIfNotEmpty("Depends On:", mapKeysToSlice(info.DependsOn), f) + if ports != nil { + addCSVIfNotEmpty("TCP Ports:", ports.TcpPorts, f) + } f.AddCheckbox("Is Disabled:", info.Disabled, nil) f.AddCheckbox("Is Daemon:", info.IsDaemon, nil) f.AddButton("Close", func() { @@ -40,12 +43,19 @@ func addStringIfNotEmpty(label, value string, f *tview.Form) { } } -func addSliceIfNotEmpty(label string, value []string, f *tview.Form) { +func addDropDownIfNotEmpty(label string, value []string, f *tview.Form) { if len(value) > 0 { f.AddDropDown(label, value, 0, nil) } } +func addCSVIfNotEmpty[K comparable](label string, value []K, f *tview.Form) { + if len(value) > 0 { + csvPorts := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(value)), ":"), "[]") + f.AddInputField(label, csvPorts, 0, nil, nil) + } +} + // mapKeysToSlice extract keys of map as slice, func mapKeysToSlice[K comparable, V any](m map[K]V) []K { keys := make([]K, len(m)) diff --git a/src/tui/view.go b/src/tui/view.go index 030fe03..9bce917 100644 --- a/src/tui/view.go +++ b/src/tui/view.go @@ -246,7 +246,8 @@ func (pv *pcView) showInfo() { pv.showError(err.Error()) return } - form := pv.createProcInfoForm(info) + ports, _ := pv.project.GetProcessPorts(name) + form := pv.createProcInfoForm(info, ports) pv.showDialog(form, 0, 0) } diff --git a/src/types/process.go b/src/types/process.go index e982d36..4af9c4c 100644 --- a/src/types/process.go +++ b/src/types/process.go @@ -83,6 +83,12 @@ type ProcessState struct { IsRunning bool } +type ProcessPorts struct { + Name string `json:"name"` + TcpPorts []uint16 `json:"tcp_ports"` + UdpPorts []uint16 `json:"udp_ports"` +} + type ProcessesState struct { States []ProcessState `json:"data"` }