detached 4c9b03f

Improve CLI features and add new root command

- Implement new root command who return local version and remote backend version
- Refactor and move some ping request logic into handler
- Use Ping.Version to new function to get backend version
- Add some color to the CLI output
- Fix some version check logic
commit
4c9b03fb8af48af788f1c9d19c4ee20c3927655f
author
bnema · · 2023-11-07 10:51 UTC
parent
5b2a192
signature
unsigned
6 files · +103 -61
M cmd/cli/main.go +1 -0
@@ -21,6 +21,7 @@ var (
2121 var rootCmd = &cobra.Command{Use: "gordon"}
2222
2323 func InitializeCommands(client *cli.App, server *server.App) {
24+ rootCmd.AddCommand(cmd.NewRootCommand(client))
2425 rootCmd.AddCommand(cmd.NewServeCommand(server))
2526 rootCmd.AddCommand(cmd.NewPingCommand(client))
2627 rootCmd.AddCommand(cmd.NewDeployCommand(client))
@@ -21,6 +21,7 @@ var (
21 var rootCmd = &cobra.Command{Use: "gordon"}21 var rootCmd = &cobra.Command{Use: "gordon"}
22 22
23 func InitializeCommands(client *cli.App, server *server.App) {23 func InitializeCommands(client *cli.App, server *server.App) {
24+ rootCmd.AddCommand(cmd.NewRootCommand(client))
24 rootCmd.AddCommand(cmd.NewServeCommand(server))25 rootCmd.AddCommand(cmd.NewServeCommand(server))
25 rootCmd.AddCommand(cmd.NewPingCommand(client))26 rootCmd.AddCommand(cmd.NewPingCommand(client))
26 rootCmd.AddCommand(cmd.NewDeployCommand(client))27 rootCmd.AddCommand(cmd.NewDeployCommand(client))
M internal/cli/cmd/ping.go +3 -51
@@ -1,26 +1,17 @@
11 package cmd
22
33 import (
4- "encoding/json"
54 "fmt"
6- "net/http"
75 "os"
86 "time"
97
108 "github.com/bnema/gordon/internal/cli"
119 "github.com/bnema/gordon/internal/cli/handler"
12- "github.com/bnema/gordon/internal/common"
1310 "github.com/bnema/gordon/pkg/humanize"
1411
1512 "github.com/spf13/cobra"
1613 )
1714
18-// Define a struct to match the JSON response structure
19-type PingResponse struct {
20- Uptime string `json:"uptime"`
21- Version string `json:"version"`
22-}
23-
2415 func NewPingCommand(a *cli.App) *cobra.Command {
2516 // handler.FieldCheck(a)
2617 return &cobra.Command{
@@ -33,47 +24,9 @@ func NewPingCommand(a *cli.App) *cobra.Command {
3324 }
3425 },
3526 Run: func(cmd *cobra.Command, args []string) {
36- data := map[string]interface{}{"message": "ping"}
37- payload, err := common.NewPingPayload(data)
38- if err != nil {
39- fmt.Println("Error creating payload:", err)
40- return
41- }
42-
43- // Create a RequestPayload and populate it
44- reqPayload := common.RequestPayload{
45- Type: "ping",
46- Payload: payload,
47- }
48-
49- resp, err := handler.SendHTTPRequest(a, &reqPayload, "GET", "/ping")
50- if err != nil {
51- fmt.Println("Error sending HTTP request:", err)
52- return
53- }
54-
55- // Check for non-200 status codes
56- if resp.StatusCode != http.StatusOK {
57- fmt.Printf("Received non-OK response from server: %s\n", resp.StatusCode)
58- return
59- }
60-
61- // Check for the correct Content-Type header
62- if contentType := resp.Header.Get("Content-Type"); contentType != "application/json" {
63- fmt.Printf("Expected application/json response, got %s\n", contentType)
64- return
65- }
66-
67- if resp.StatusCode == 401 || resp.StatusCode == 403 || resp.StatusCode == 404 {
68- fmt.Println("Login incorrect, edit your config file and try again")
69- return
70- }
71-
72- // Unmarshal the JSON response into the PingResponse struct
73- var pingResp PingResponse
74- err = json.Unmarshal(resp.Body, &pingResp)
27+ pingResp, err := handler.PerformPingRequest(a)
7528 if err != nil {
76- fmt.Println("Error unmarshalling response:", err)
29+ fmt.Println("Error:", err)
7730 return
7831 }
7932
@@ -90,9 +43,8 @@ func NewPingCommand(a *cli.App) *cobra.Command {
9043 // Use humanize to get a human-readable representation of the start time
9144 humanizedUptime := humanize.TimeAgo(startTime)
9245
93- /// Print the information
46+ // Print the information
9447 fmt.Printf("Pong!\nServer up since: %s\nServer version: %s\n", humanizedUptime, pingResp.Version)
95-
9648 },
9749 }
9850 }
@@ -1,26 +1,17 @@
1 package cmd1 package cmd
2 2
3 import (3 import (
4- "encoding/json"
5 "fmt"4 "fmt"
6- "net/http"
7 "os"5 "os"
8 "time"6 "time"
9 7
10 "github.com/bnema/gordon/internal/cli"8 "github.com/bnema/gordon/internal/cli"
11 "github.com/bnema/gordon/internal/cli/handler"9 "github.com/bnema/gordon/internal/cli/handler"
12- "github.com/bnema/gordon/internal/common"
13 "github.com/bnema/gordon/pkg/humanize"10 "github.com/bnema/gordon/pkg/humanize"
14 11
15 "github.com/spf13/cobra"12 "github.com/spf13/cobra"
16 )13 )
17 14
18-// Define a struct to match the JSON response structure
19-type PingResponse struct {
20- Uptime string `json:"uptime"`
21- Version string `json:"version"`
22-}
23-
24 func NewPingCommand(a *cli.App) *cobra.Command {15 func NewPingCommand(a *cli.App) *cobra.Command {
25 // handler.FieldCheck(a)16 // handler.FieldCheck(a)
26 return &cobra.Command{17 return &cobra.Command{
@@ -33,47 +24,9 @@ func NewPingCommand(a *cli.App) *cobra.Command {
33 }24 }
34 },25 },
35 Run: func(cmd *cobra.Command, args []string) {26 Run: func(cmd *cobra.Command, args []string) {
36- data := map[string]interface{}{"message": "ping"}27+ pingResp, err := handler.PerformPingRequest(a)
37- payload, err := common.NewPingPayload(data)
38- if err != nil {
39- fmt.Println("Error creating payload:", err)
40- return
41- }
42-
43- // Create a RequestPayload and populate it
44- reqPayload := common.RequestPayload{
45- Type: "ping",
46- Payload: payload,
47- }
48-
49- resp, err := handler.SendHTTPRequest(a, &reqPayload, "GET", "/ping")
50- if err != nil {
51- fmt.Println("Error sending HTTP request:", err)
52- return
53- }
54-
55- // Check for non-200 status codes
56- if resp.StatusCode != http.StatusOK {
57- fmt.Printf("Received non-OK response from server: %s\n", resp.StatusCode)
58- return
59- }
60-
61- // Check for the correct Content-Type header
62- if contentType := resp.Header.Get("Content-Type"); contentType != "application/json" {
63- fmt.Printf("Expected application/json response, got %s\n", contentType)
64- return
65- }
66-
67- if resp.StatusCode == 401 || resp.StatusCode == 403 || resp.StatusCode == 404 {
68- fmt.Println("Login incorrect, edit your config file and try again")
69- return
70- }
71-
72- // Unmarshal the JSON response into the PingResponse struct
73- var pingResp PingResponse
74- err = json.Unmarshal(resp.Body, &pingResp)
75 if err != nil {28 if err != nil {
76- fmt.Println("Error unmarshalling response:", err)29+ fmt.Println("Error:", err)
77 return30 return
78 }31 }
79 32
@@ -90,9 +43,8 @@ func NewPingCommand(a *cli.App) *cobra.Command {
90 // Use humanize to get a human-readable representation of the start time43 // Use humanize to get a human-readable representation of the start time
91 humanizedUptime := humanize.TimeAgo(startTime)44 humanizedUptime := humanize.TimeAgo(startTime)
92 45
93- /// Print the information46+ // Print the information
94 fmt.Printf("Pong!\nServer up since: %s\nServer version: %s\n", humanizedUptime, pingResp.Version)47 fmt.Printf("Pong!\nServer up since: %s\nServer version: %s\n", humanizedUptime, pingResp.Version)
95-
96 },48 },
97 }49 }
98 }50 }
M internal/cli/cmd/root.go +37 -5
@@ -1,8 +1,40 @@
11 // the root command is the entrypoint for the gordon cli (default: client)
22 package cmd
33
4-// if no command selected print all commands
5-// available commands for now are:
6-// - deploy
7-// - serve
8-// - version
4+import (
5+ "fmt"
6+
7+ "github.com/bnema/gordon/internal/cli"
8+ "github.com/bnema/gordon/internal/cli/handler"
9+ "github.com/fatih/color"
10+ "github.com/spf13/cobra"
11+)
12+
13+// NewRootCommand creates a new root command
14+func NewRootCommand(a *cli.App) *cobra.Command {
15+ cmd := &cobra.Command{
16+ Use: "gordon",
17+ Short: "Gordon is a CLI for the Gordon project",
18+ }
19+
20+ backendVersion, err := getBackendVersion(a)
21+ if err != nil {
22+ fmt.Println("Error getting backend version:", err)
23+ return cmd
24+ }
25+
26+ // Using color.Green function to print in green
27+ color.Green("Gordon %s", a.Config.GetVersion())
28+ color.Blue("Gordon Backend %s", backendVersion)
29+
30+ return cmd
31+}
32+
33+func getBackendVersion(a *cli.App) (string, error) {
34+ pingResp, err := handler.PerformPingRequest(a)
35+ if err != nil {
36+ return "", err
37+ }
38+
39+ return pingResp.Version, nil
40+}
@@ -1,8 +1,40 @@
1 // the root command is the entrypoint for the gordon cli (default: client)1 // the root command is the entrypoint for the gordon cli (default: client)
2 package cmd2 package cmd
3 3
4-// if no command selected print all commands4+import (
5-// available commands for now are:5+ "fmt"
6-// - deploy6+
7-// - serve7+ "github.com/bnema/gordon/internal/cli"
8-// - version8+ "github.com/bnema/gordon/internal/cli/handler"
9+ "github.com/fatih/color"
10+ "github.com/spf13/cobra"
11+)
12+
13+// NewRootCommand creates a new root command
14+func NewRootCommand(a *cli.App) *cobra.Command {
15+ cmd := &cobra.Command{
16+ Use: "gordon",
17+ Short: "Gordon is a CLI for the Gordon project",
18+ }
19+
20+ backendVersion, err := getBackendVersion(a)
21+ if err != nil {
22+ fmt.Println("Error getting backend version:", err)
23+ return cmd
24+ }
25+
26+ // Using color.Green function to print in green
27+ color.Green("Gordon %s", a.Config.GetVersion())
28+ color.Blue("Gordon Backend %s", backendVersion)
29+
30+ return cmd
31+}
32+
33+func getBackendVersion(a *cli.App) (string, error) {
34+ pingResp, err := handler.PerformPingRequest(a)
35+ if err != nil {
36+ return "", err
37+ }
38+
39+ return pingResp.Version, nil
40+}
A internal/cli/handler/pingrequest.go +54 -0
@@ -0,0 +1,54 @@
1+package handler
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "net/http"
7+
8+ "github.com/bnema/gordon/internal/cli"
9+ "github.com/bnema/gordon/internal/common"
10+)
11+
12+// Define a struct to match the JSON response structure
13+type PingResponse struct {
14+ Uptime string `json:"uptime"`
15+ Version string `json:"version"`
16+}
17+
18+func PerformPingRequest(a *cli.App) (PingResponse, error) {
19+ var pingResp PingResponse
20+
21+ data := map[string]interface{}{"message": "ping"}
22+ payload, err := common.NewPingPayload(data)
23+ if err != nil {
24+ return pingResp, fmt.Errorf("failed to create new ping payload: %w", err)
25+ }
26+
27+ // Create a RequestPayload and populate it
28+ reqPayload := common.RequestPayload{
29+ Type: "ping",
30+ Payload: payload,
31+ }
32+
33+ resp, err := SendHTTPRequest(a, &reqPayload, "GET", "/ping")
34+ if err != nil {
35+ return pingResp, fmt.Errorf("failed to reach backend")
36+ }
37+
38+ defer resp.Http.Body.Close()
39+
40+ if resp.StatusCode != http.StatusOK {
41+ return PingResponse{
42+ Uptime: "",
43+ Version: "",
44+ }, fmt.Errorf("expected status code 200, got %d", resp.StatusCode)
45+ }
46+
47+ // Unmarshal the JSON response into the PingResponse struct
48+ err = json.Unmarshal(resp.Body, &pingResp)
49+ if err != nil {
50+ return pingResp, err
51+ }
52+
53+ return pingResp, nil
54+}
@@ -0,0 +1,54 @@
1+package handler
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "net/http"
7+
8+ "github.com/bnema/gordon/internal/cli"
9+ "github.com/bnema/gordon/internal/common"
10+)
11+
12+// Define a struct to match the JSON response structure
13+type PingResponse struct {
14+ Uptime string `json:"uptime"`
15+ Version string `json:"version"`
16+}
17+
18+func PerformPingRequest(a *cli.App) (PingResponse, error) {
19+ var pingResp PingResponse
20+
21+ data := map[string]interface{}{"message": "ping"}
22+ payload, err := common.NewPingPayload(data)
23+ if err != nil {
24+ return pingResp, fmt.Errorf("failed to create new ping payload: %w", err)
25+ }
26+
27+ // Create a RequestPayload and populate it
28+ reqPayload := common.RequestPayload{
29+ Type: "ping",
30+ Payload: payload,
31+ }
32+
33+ resp, err := SendHTTPRequest(a, &reqPayload, "GET", "/ping")
34+ if err != nil {
35+ return pingResp, fmt.Errorf("failed to reach backend")
36+ }
37+
38+ defer resp.Http.Body.Close()
39+
40+ if resp.StatusCode != http.StatusOK {
41+ return PingResponse{
42+ Uptime: "",
43+ Version: "",
44+ }, fmt.Errorf("expected status code 200, got %d", resp.StatusCode)
45+ }
46+
47+ // Unmarshal the JSON response into the PingResponse struct
48+ err = json.Unmarshal(resp.Body, &pingResp)
49+ if err != nil {
50+ return pingResp, err
51+ }
52+
53+ return pingResp, nil
54+}
M internal/common/version.go +8 -3
@@ -10,6 +10,7 @@ import (
1010 "time"
1111
1212 "github.com/bnema/gordon/pkg/docker"
13+ "github.com/fatih/color"
1314 )
1415
1516 type CurrentVersion struct {
@@ -66,14 +67,18 @@ func checkAndUpdateVersion(c *Config) (string, error) {
6667 remoteVersion = remoteVersions.Name
6768 }
6869
69- fmt.Printf("Local version: %s\n", localVersion.Version)
7070 remoteVersion = remoteVersion[:len(remoteVersion)-len(getArch())-1]
71- fmt.Printf("Remote version: %s\n", remoteVersion)
7271 message := CheckForNewVersion(localVersion.Version, remoteVersion)
72+
7373 if message != "" {
74- return fmt.Sprintf("New version %s is available", remoteVersion), nil
74+ // Create a new color attribute
75+ green := color.New(color.FgGreen).SprintFunc()
76+ // Use the color attribute to format the string with ANSI codes
77+ coloredMessage := green(fmt.Sprintf("New version %s is available", remoteVersion))
78+ return coloredMessage, nil
7579 }
7680 return "", nil
81+
7782 }
7883
7984 func CheckVersionPeriodically(c *Config) (string, error) {
@@ -10,6 +10,7 @@ import (
10 "time"10 "time"
11 11
12 "github.com/bnema/gordon/pkg/docker"12 "github.com/bnema/gordon/pkg/docker"
13+ "github.com/fatih/color"
13 )14 )
14 15
15 type CurrentVersion struct {16 type CurrentVersion struct {
@@ -66,14 +67,18 @@ func checkAndUpdateVersion(c *Config) (string, error) {
66 remoteVersion = remoteVersions.Name67 remoteVersion = remoteVersions.Name
67 }68 }
68 69
69- fmt.Printf("Local version: %s\n", localVersion.Version)
70 remoteVersion = remoteVersion[:len(remoteVersion)-len(getArch())-1]70 remoteVersion = remoteVersion[:len(remoteVersion)-len(getArch())-1]
71- fmt.Printf("Remote version: %s\n", remoteVersion)
72 message := CheckForNewVersion(localVersion.Version, remoteVersion)71 message := CheckForNewVersion(localVersion.Version, remoteVersion)
72+
73 if message != "" {73 if message != "" {
74- return fmt.Sprintf("New version %s is available", remoteVersion), nil74+ // Create a new color attribute
75+ green := color.New(color.FgGreen).SprintFunc()
76+ // Use the color attribute to format the string with ANSI codes
77+ coloredMessage := green(fmt.Sprintf("New version %s is available", remoteVersion))
78+ return coloredMessage, nil
75 }79 }
76 return "", nil80 return "", nil
81+
77 }82 }
78 83
79 func CheckVersionPeriodically(c *Config) (string, error) {84 func CheckVersionPeriodically(c *Config) (string, error) {
M internal/httpserve/middleware/secure.go +0 -2
@@ -1,7 +1,6 @@
11 package middleware
22
33 import (
4- "fmt"
54 "log"
65 "os"
76
@@ -16,7 +15,6 @@ import (
1615 func SecureRoutes(a *server.App) echo.MiddlewareFunc {
1716 proxyURL := a.Config.Build.ProxyURL // <-- https://gordon-proxy.bnema.dev
1817 urlCheckVersion := a.Config.Build.ProxyURL + "/version"
19- fmt.Println("Proxy URL:", proxyURL)
2018 return middleware.SecureWithConfig(middleware.SecureConfig{
2119 XSSProtection: "1; mode=block",
2220 ContentTypeNosniff: "nosniff",
@@ -1,7 +1,6 @@
1 package middleware1 package middleware
2 2
3 import (3 import (
4- "fmt"
5 "log"4 "log"
6 "os"5 "os"
7 6
@@ -16,7 +15,6 @@ import (
16 func SecureRoutes(a *server.App) echo.MiddlewareFunc {15 func SecureRoutes(a *server.App) echo.MiddlewareFunc {
17 proxyURL := a.Config.Build.ProxyURL // <-- https://gordon-proxy.bnema.dev16 proxyURL := a.Config.Build.ProxyURL // <-- https://gordon-proxy.bnema.dev
18 urlCheckVersion := a.Config.Build.ProxyURL + "/version"17 urlCheckVersion := a.Config.Build.ProxyURL + "/version"
19- fmt.Println("Proxy URL:", proxyURL)
20 return middleware.SecureWithConfig(middleware.SecureConfig{18 return middleware.SecureWithConfig(middleware.SecureConfig{
21 XSSProtection: "1; mode=block",19 XSSProtection: "1; mode=block",
22 ContentTypeNosniff: "nosniff",20 ContentTypeNosniff: "nosniff",

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close