detached 8b3a4f8

push: Implement a new push command to simply push an image

commit
8b3a4f8056ca8e89dd31a084e91c5a35e9256a9f
author
bnema · · 2024-04-22 08:44 UTC
parent
d511321
signature
unsigned
5 files · +183 -14
M cmd/cli/main.go +1 -0
@@ -30,6 +30,7 @@ func InitializeCommands(client *cli.App, server *server.App) {
3030 rootCmd.AddCommand(cmd.NewPingCommand(client))
3131 rootCmd.AddCommand(cmd.NewDeployCommand(client))
3232 rootCmd.AddCommand(cmd.NewUpdateCommand(client))
33+ rootCmd.AddCommand(cmd.NewPushCommand(client))
3334 }
3435
3536 func Execute(client *cli.App, server *server.App) {
@@ -30,6 +30,7 @@ func InitializeCommands(client *cli.App, server *server.App) {
30 rootCmd.AddCommand(cmd.NewPingCommand(client))30 rootCmd.AddCommand(cmd.NewPingCommand(client))
31 rootCmd.AddCommand(cmd.NewDeployCommand(client))31 rootCmd.AddCommand(cmd.NewDeployCommand(client))
32 rootCmd.AddCommand(cmd.NewUpdateCommand(client))32 rootCmd.AddCommand(cmd.NewUpdateCommand(client))
33+ rootCmd.AddCommand(cmd.NewPushCommand(client))
33 }34 }
34 35
35 func Execute(client *cli.App, server *server.App) {36 func Execute(client *cli.App, server *server.App) {
M internal/cli/cmd/deploy.go +14 -9
@@ -59,12 +59,14 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
5959 }
6060
6161 // Export the image to a reader and return its true size
62- reader, actualSize, err := exportDockerImage(imageName)
62+ reader, actualSize, successMsg, err := exportDockerImage(imageName)
6363 if err != nil {
6464 fmt.Println("Error exporting image:", err)
6565 return
6666 }
6767
68+ fmt.Println(successMsg)
69+
6870 progressCh := make(chan mvu.ProgressMsg)
6971 errCh := make(chan error, 1) // Buffer of 1 to prevent goroutine leak in case of non-blocking send
7072
@@ -107,7 +109,7 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
107109
108110 // Initialize HTTP client
109111 client := &http.Client{
110- Timeout: 5 * time.Second,
112+ Timeout: 60 * time.Second,
111113 }
112114
113115 // Only set TLS config if target is HTTPS
@@ -176,11 +178,11 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
176178 }
177179
178180 // export the docker image to a reader and return its true size
179-func exportDockerImage(imageName string) (io.ReadCloser, int64, error) {
181+func exportDockerImage(imageName string) (io.ReadCloser, int64, string, error) {
180182 // Check if what the user submitted is a valid image ID
181183 exists, err := docker.CheckIfImageExists(imageName)
182184 if err != nil {
183- return nil, 0, fmt.Errorf("error while checking image existence: %w", err)
185+ return nil, 0, "", fmt.Errorf("error while checking image existence: %w", err)
184186 }
185187
186188 var imageID string
@@ -191,20 +193,23 @@ func exportDockerImage(imageName string) (io.ReadCloser, int64, error) {
191193 // What the user submitted is not a valid image ID, search by name
192194 imageID, err = docker.GetImageIDByName(imageName)
193195 if err != nil {
194- return nil, 0, fmt.Errorf("error while searching for image by name: %w", err)
196+ return nil, 0, "", fmt.Errorf("error while searching for image by name: %w", err)
195197 }
196198 }
197199
198200 actualSize, err := docker.GetImageSizeFromReader(imageID)
199201 if err != nil {
200- return nil, 0, fmt.Errorf("error while retrieving image size: %w", err)
202+ return nil, 0, "", fmt.Errorf("error while retrieving image size: %w", err)
201203 }
202204
203205 reader, err := docker.ExportImageFromEngine(imageID)
204206 if err != nil {
205- return nil, 0, fmt.Errorf("error while exporting image: %w", err)
207+ return nil, 0, "", fmt.Errorf("error while exporting image: %w", err)
206208 }
207209
208- // Return the reader and the actual size
209- return reader, actualSize, nil
210+ // Create a success message
211+ successMsg := fmt.Sprintf("Image %s exported successfully", imageName)
212+
213+ // Return the reader, actual size, and success message
214+ return reader, actualSize, successMsg, nil
210215 }
@@ -59,12 +59,14 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
59 }59 }
60 60
61 // Export the image to a reader and return its true size61 // Export the image to a reader and return its true size
62- reader, actualSize, err := exportDockerImage(imageName)62+ reader, actualSize, successMsg, err := exportDockerImage(imageName)
63 if err != nil {63 if err != nil {
64 fmt.Println("Error exporting image:", err)64 fmt.Println("Error exporting image:", err)
65 return65 return
66 }66 }
67 67
68+ fmt.Println(successMsg)
69+
68 progressCh := make(chan mvu.ProgressMsg)70 progressCh := make(chan mvu.ProgressMsg)
69 errCh := make(chan error, 1) // Buffer of 1 to prevent goroutine leak in case of non-blocking send71 errCh := make(chan error, 1) // Buffer of 1 to prevent goroutine leak in case of non-blocking send
70 72
@@ -107,7 +109,7 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
107 109
108 // Initialize HTTP client110 // Initialize HTTP client
109 client := &http.Client{111 client := &http.Client{
110- Timeout: 5 * time.Second,112+ Timeout: 60 * time.Second,
111 }113 }
112 114
113 // Only set TLS config if target is HTTPS115 // Only set TLS config if target is HTTPS
@@ -176,11 +178,11 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
176 }178 }
177 179
178 // export the docker image to a reader and return its true size180 // export the docker image to a reader and return its true size
179-func exportDockerImage(imageName string) (io.ReadCloser, int64, error) {181+func exportDockerImage(imageName string) (io.ReadCloser, int64, string, error) {
180 // Check if what the user submitted is a valid image ID182 // Check if what the user submitted is a valid image ID
181 exists, err := docker.CheckIfImageExists(imageName)183 exists, err := docker.CheckIfImageExists(imageName)
182 if err != nil {184 if err != nil {
183- return nil, 0, fmt.Errorf("error while checking image existence: %w", err)185+ return nil, 0, "", fmt.Errorf("error while checking image existence: %w", err)
184 }186 }
185 187
186 var imageID string188 var imageID string
@@ -191,20 +193,23 @@ func exportDockerImage(imageName string) (io.ReadCloser, int64, error) {
191 // What the user submitted is not a valid image ID, search by name193 // What the user submitted is not a valid image ID, search by name
192 imageID, err = docker.GetImageIDByName(imageName)194 imageID, err = docker.GetImageIDByName(imageName)
193 if err != nil {195 if err != nil {
194- return nil, 0, fmt.Errorf("error while searching for image by name: %w", err)196+ return nil, 0, "", fmt.Errorf("error while searching for image by name: %w", err)
195 }197 }
196 }198 }
197 199
198 actualSize, err := docker.GetImageSizeFromReader(imageID)200 actualSize, err := docker.GetImageSizeFromReader(imageID)
199 if err != nil {201 if err != nil {
200- return nil, 0, fmt.Errorf("error while retrieving image size: %w", err)202+ return nil, 0, "", fmt.Errorf("error while retrieving image size: %w", err)
201 }203 }
202 204
203 reader, err := docker.ExportImageFromEngine(imageID)205 reader, err := docker.ExportImageFromEngine(imageID)
204 if err != nil {206 if err != nil {
205- return nil, 0, fmt.Errorf("error while exporting image: %w", err)207+ return nil, 0, "", fmt.Errorf("error while exporting image: %w", err)
206 }208 }
207 209
208- // Return the reader and the actual size210+ // Create a success message
209- return reader, actualSize, nil211+ successMsg := fmt.Sprintf("Image %s exported successfully", imageName)
212+
213+ // Return the reader, actual size, and success message
214+ return reader, actualSize, successMsg, nil
210 }215 }
A internal/cli/cmd/push.go +119 -0
@@ -0,0 +1,119 @@
1+package cmd
2+
3+import (
4+ "bytes"
5+ "crypto/tls"
6+ "fmt"
7+ "io"
8+ "net/http"
9+ "os"
10+ "strings"
11+ "sync"
12+ "time"
13+
14+ "github.com/bnema/gordon/internal/cli"
15+ "github.com/bnema/gordon/internal/cli/handler"
16+ "github.com/bnema/gordon/internal/common"
17+ "github.com/fatih/color"
18+ "github.com/spf13/cobra"
19+)
20+
21+func NewPushCommand(a *cli.App) *cobra.Command {
22+ pushCmd := &cobra.Command{
23+ Use: "push [image:tag]",
24+ Short: "Push an image to your remote Gordon instance",
25+ Args: cobra.ExactArgs(1),
26+ PreRun: func(cmd *cobra.Command, args []string) {
27+ if err := handler.FieldCheck(a); err != nil {
28+ fmt.Println("Field check failed:", err)
29+ os.Exit(1)
30+ }
31+ },
32+ Run: func(cmd *cobra.Command, args []string) {
33+ imageName := args[0]
34+ color.White("Pushing image: %s", imageName)
35+
36+ // Validate the image name
37+ if err := handler.ValidateImageName(imageName); err != nil {
38+ fmt.Println(err)
39+ return
40+ }
41+
42+ // Ensure the image has a tag
43+ handler.EnsureImageTag(&imageName)
44+
45+ // Export the image to a reader and return its true size
46+ reader, _, successMsg, err := exportDockerImage(imageName)
47+ if err != nil {
48+ fmt.Println("Error exporting image:", err)
49+ return
50+ }
51+
52+ fmt.Println(successMsg)
53+
54+ // Create a progress bar
55+ progressBar := cmd.OutOrStdout()
56+ fmt.Fprintf(progressBar, "Uploading image... ")
57+
58+ // Create a RequestPayload and populate it
59+ reqPayload := common.RequestPayload{
60+ Type: "push",
61+ Payload: common.PushPayload{
62+ ImageName: imageName,
63+ Data: reader,
64+ },
65+ }
66+
67+ var wg sync.WaitGroup
68+ wg.Add(1)
69+
70+ go func() {
71+ defer wg.Done()
72+ resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")
73+ if err != nil {
74+ fmt.Println("Error sending request:", err)
75+ return
76+ }
77+
78+ if resp.StatusCode != http.StatusOK {
79+ bodyBytes, _ := io.ReadAll(bytes.NewReader(resp.Body))
80+ fmt.Fprintln(cmd.OutOrStderr(), "Server returned an error:", string(bodyBytes))
81+ return
82+ }
83+
84+ // Check the response
85+ targetDomain := string(resp.Body)
86+ targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace
87+ targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes
88+ // Close the progress channel after the upload is complete
89+ // Determine if the target is HTTPS or HTTP
90+ isHTTPS := strings.HasPrefix(targetDomain, "https://")
91+
92+ // Initialize HTTP client
93+ client := &http.Client{
94+ Timeout: 10 * time.Second,
95+ }
96+
97+ // Only set TLS config if target is HTTPS
98+ if isHTTPS {
99+ client.Transport = &http.Transport{
100+ TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated
101+ }
102+ }
103+
104+ }()
105+
106+ // Wait for the HTTP request to finish
107+ wg.Wait()
108+
109+ // Close the reader
110+ err = reader.Close()
111+ if err != nil {
112+ fmt.Println("Error closing reader:", err)
113+ return
114+ }
115+ },
116+ }
117+
118+ return pushCmd
119+}
@@ -0,0 +1,119 @@
1+package cmd
2+
3+import (
4+ "bytes"
5+ "crypto/tls"
6+ "fmt"
7+ "io"
8+ "net/http"
9+ "os"
10+ "strings"
11+ "sync"
12+ "time"
13+
14+ "github.com/bnema/gordon/internal/cli"
15+ "github.com/bnema/gordon/internal/cli/handler"
16+ "github.com/bnema/gordon/internal/common"
17+ "github.com/fatih/color"
18+ "github.com/spf13/cobra"
19+)
20+
21+func NewPushCommand(a *cli.App) *cobra.Command {
22+ pushCmd := &cobra.Command{
23+ Use: "push [image:tag]",
24+ Short: "Push an image to your remote Gordon instance",
25+ Args: cobra.ExactArgs(1),
26+ PreRun: func(cmd *cobra.Command, args []string) {
27+ if err := handler.FieldCheck(a); err != nil {
28+ fmt.Println("Field check failed:", err)
29+ os.Exit(1)
30+ }
31+ },
32+ Run: func(cmd *cobra.Command, args []string) {
33+ imageName := args[0]
34+ color.White("Pushing image: %s", imageName)
35+
36+ // Validate the image name
37+ if err := handler.ValidateImageName(imageName); err != nil {
38+ fmt.Println(err)
39+ return
40+ }
41+
42+ // Ensure the image has a tag
43+ handler.EnsureImageTag(&imageName)
44+
45+ // Export the image to a reader and return its true size
46+ reader, _, successMsg, err := exportDockerImage(imageName)
47+ if err != nil {
48+ fmt.Println("Error exporting image:", err)
49+ return
50+ }
51+
52+ fmt.Println(successMsg)
53+
54+ // Create a progress bar
55+ progressBar := cmd.OutOrStdout()
56+ fmt.Fprintf(progressBar, "Uploading image... ")
57+
58+ // Create a RequestPayload and populate it
59+ reqPayload := common.RequestPayload{
60+ Type: "push",
61+ Payload: common.PushPayload{
62+ ImageName: imageName,
63+ Data: reader,
64+ },
65+ }
66+
67+ var wg sync.WaitGroup
68+ wg.Add(1)
69+
70+ go func() {
71+ defer wg.Done()
72+ resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")
73+ if err != nil {
74+ fmt.Println("Error sending request:", err)
75+ return
76+ }
77+
78+ if resp.StatusCode != http.StatusOK {
79+ bodyBytes, _ := io.ReadAll(bytes.NewReader(resp.Body))
80+ fmt.Fprintln(cmd.OutOrStderr(), "Server returned an error:", string(bodyBytes))
81+ return
82+ }
83+
84+ // Check the response
85+ targetDomain := string(resp.Body)
86+ targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace
87+ targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes
88+ // Close the progress channel after the upload is complete
89+ // Determine if the target is HTTPS or HTTP
90+ isHTTPS := strings.HasPrefix(targetDomain, "https://")
91+
92+ // Initialize HTTP client
93+ client := &http.Client{
94+ Timeout: 10 * time.Second,
95+ }
96+
97+ // Only set TLS config if target is HTTPS
98+ if isHTTPS {
99+ client.Transport = &http.Transport{
100+ TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated
101+ }
102+ }
103+
104+ }()
105+
106+ // Wait for the HTTP request to finish
107+ wg.Wait()
108+
109+ // Close the reader
110+ err = reader.Close()
111+ if err != nil {
112+ fmt.Println("Error closing reader:", err)
113+ return
114+ }
115+ },
116+ }
117+
118+ return pushCmd
119+}
M internal/cli/handler/http.go +31 -0
@@ -81,6 +81,37 @@ func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpo
8181 }
8282
8383 // Handle other types of payloads
84+ if rp.Type != "push" {
85+ pushPayload, ok := rp.Payload.(common.PushPayload)
86+ if !ok {
87+ return nil, fmt.Errorf("invalid payload type for push")
88+ }
89+
90+ req, err = http.NewRequest(method, apiUrl+endpoint, pushPayload.Data)
91+ if err != nil {
92+ return nil, fmt.Errorf("failed to create new streaming request: %w", err)
93+ }
94+
95+ setAuthRequestHeaders(req, token)
96+ req.Header.Set("Content-Type", "application/octet-stream")
97+ req.Header.Set("X-Image-Name", pushPayload.ImageName)
98+
99+ client := &http.Client{}
100+ resp, err := client.Do(req)
101+ if err != nil {
102+ return nil, fmt.Errorf("failed to send request: %w", err)
103+ }
104+
105+ defer resp.Body.Close()
106+
107+ body, err := io.ReadAll(resp.Body)
108+ if err != nil {
109+ return nil, fmt.Errorf("failed to read response body: %w", err)
110+ }
111+
112+ return &Response{Body: body}, nil
113+ }
114+
84115 jsonPayload, err := json.Marshal(rp)
85116 if err != nil {
86117 return nil, fmt.Errorf("failed to marshal payload: %w", err)
@@ -81,6 +81,37 @@ func SendHTTPRequest(a *cli.App, rp *common.RequestPayload, method string, endpo
81 }81 }
82 82
83 // Handle other types of payloads83 // Handle other types of payloads
84+ if rp.Type != "push" {
85+ pushPayload, ok := rp.Payload.(common.PushPayload)
86+ if !ok {
87+ return nil, fmt.Errorf("invalid payload type for push")
88+ }
89+
90+ req, err = http.NewRequest(method, apiUrl+endpoint, pushPayload.Data)
91+ if err != nil {
92+ return nil, fmt.Errorf("failed to create new streaming request: %w", err)
93+ }
94+
95+ setAuthRequestHeaders(req, token)
96+ req.Header.Set("Content-Type", "application/octet-stream")
97+ req.Header.Set("X-Image-Name", pushPayload.ImageName)
98+
99+ client := &http.Client{}
100+ resp, err := client.Do(req)
101+ if err != nil {
102+ return nil, fmt.Errorf("failed to send request: %w", err)
103+ }
104+
105+ defer resp.Body.Close()
106+
107+ body, err := io.ReadAll(resp.Body)
108+ if err != nil {
109+ return nil, fmt.Errorf("failed to read response body: %w", err)
110+ }
111+
112+ return &Response{Body: body}, nil
113+ }
114+
84 jsonPayload, err := json.Marshal(rp)115 jsonPayload, err := json.Marshal(rp)
85 if err != nil {116 if err != nil {
86 return nil, fmt.Errorf("failed to marshal payload: %w", err)117 return nil, fmt.Errorf("failed to marshal payload: %w", err)
M internal/common/payload.go +18 -5
@@ -40,7 +40,16 @@ func (p *RequestPayload) UnmarshalJSON(data []byte) error {
4040 return err
4141 }
4242 p.Payload = payload
43+ case "push":
44+ var payload PushPayload
45+ if err := json.Unmarshal(raw["payload"], &payload); err != nil {
46+ return err
47+ }
48+ p.Payload = payload
49+ default:
50+ return fmt.Errorf("invalid type: %s", p.Type)
4351 }
52+
4453 return nil
4554 }
4655
@@ -56,6 +65,10 @@ func (p DeployPayload) GetType() string {
5665 return "deploy"
5766 }
5867
68+func (p PushPayload) GetType() string {
69+ return "push"
70+}
71+
5972 func NewPingPayload(data map[string]interface{}) (PingPayload, error) {
6073 message, ok := data["message"].(string)
6174 if !ok {
@@ -64,11 +77,6 @@ func NewPingPayload(data map[string]interface{}) (PingPayload, error) {
6477 return PingPayload{Message: message}, nil
6578 }
6679
67-// What do we need inside the deploy payload :
68-// the ports as a string (80:80/tcp, 443:443/tcp)
69-// the image+tag as a string (nginx:latest)
70-// the .tar container image as a byte array
71-
7280 type DeployPayload struct {
7381 Ports string `json:"ports"`
7482 TargetDomain string `json:"targetdomain"`
@@ -76,3 +84,8 @@ type DeployPayload struct {
7684 ImageID string `json:"imageid"`
7785 Data io.ReadCloser
7886 }
87+
88+type PushPayload struct {
89+ ImageName string `json:"imagename"`
90+ Data io.ReadCloser
91+}
@@ -40,7 +40,16 @@ func (p *RequestPayload) UnmarshalJSON(data []byte) error {
40 return err40 return err
41 }41 }
42 p.Payload = payload42 p.Payload = payload
43+ case "push":
44+ var payload PushPayload
45+ if err := json.Unmarshal(raw["payload"], &payload); err != nil {
46+ return err
47+ }
48+ p.Payload = payload
49+ default:
50+ return fmt.Errorf("invalid type: %s", p.Type)
43 }51 }
52+
44 return nil53 return nil
45 }54 }
46 55
@@ -56,6 +65,10 @@ func (p DeployPayload) GetType() string {
56 return "deploy"65 return "deploy"
57 }66 }
58 67
68+func (p PushPayload) GetType() string {
69+ return "push"
70+}
71+
59 func NewPingPayload(data map[string]interface{}) (PingPayload, error) {72 func NewPingPayload(data map[string]interface{}) (PingPayload, error) {
60 message, ok := data["message"].(string)73 message, ok := data["message"].(string)
61 if !ok {74 if !ok {
@@ -64,11 +77,6 @@ func NewPingPayload(data map[string]interface{}) (PingPayload, error) {
64 return PingPayload{Message: message}, nil77 return PingPayload{Message: message}, nil
65 }78 }
66 79
67-// What do we need inside the deploy payload :
68-// the ports as a string (80:80/tcp, 443:443/tcp)
69-// the image+tag as a string (nginx:latest)
70-// the .tar container image as a byte array
71-
72 type DeployPayload struct {80 type DeployPayload struct {
73 Ports string `json:"ports"`81 Ports string `json:"ports"`
74 TargetDomain string `json:"targetdomain"`82 TargetDomain string `json:"targetdomain"`
@@ -76,3 +84,8 @@ type DeployPayload struct {
76 ImageID string `json:"imageid"`84 ImageID string `json:"imageid"`
77 Data io.ReadCloser85 Data io.ReadCloser
78 }86 }
87+
88+type PushPayload struct {
89+ ImageName string `json:"imagename"`
90+ Data io.ReadCloser
91+}

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