1package cmd
2
3import (
4 "crypto/tls"
5 "fmt"
6 "io"
7 "net/http"
8 "os"
9 "strings"
10 "time"
11
12 "github.com/bnema/gordon/internal/cli"
13 "github.com/bnema/gordon/internal/cli/handler"
14 "github.com/bnema/gordon/internal/cli/mvu"
15 "github.com/bnema/gordon/internal/common"
16 "github.com/bnema/gordon/pkg/docker"
17 "github.com/fatih/color"
18 "github.com/spf13/cobra"
19)
20
21func NewDeployCommand(a *cli.App) *cobra.Command {
22 var port string
23 var targetDomain string
24
25 deployCmd := &cobra.Command{
26 Use: "deploy [image:tag]",
27 Short: "Deploy an image to your remote Gordon instance",
28 Args: cobra.ExactArgs(1),
29 PreRun: func(cmd *cobra.Command, args []string) {
30 if err := handler.FieldCheck(a); err != nil {
31 fmt.Println("Field check failed:", err)
32 os.Exit(1)
33 }
34 },
35 Run: func(cmd *cobra.Command, args []string) {
36 imageName := args[0]
37 color.White("Pushing image: %s", imageName)
38
39 // Validate the image name
40 if err := handler.ValidateImageName(imageName); err != nil {
41 fmt.Println(err)
42 return
43 }
44
45 // Ensure the image has a tag
46 handler.EnsureImageTag(&imageName)
47
48 // Validate the port mapping
49 if err := handler.ValidatePortMapping(port); err != nil {
50 fmt.Println(err)
51 return
52 }
53
54 // Validate the target domain
55 if err := handler.ValidateTargetDomain(targetDomain); err != nil {
56 fmt.Println(err)
57 return
58 }
59
60 // Export the image to a reader and return its true size
61 reader, actualSize, err := exportDockerImage(imageName)
62 if err != nil {
63 fmt.Println("Error exporting image:", err)
64 return
65 }
66
67 progressCh := make(chan mvu.ProgressMsg)
68 errCh := make(chan error, 1) // Buffer of 1 to prevent goroutine leak in case of non-blocking send
69
70 // Create a progress function to update the progress bar
71 progressReader := &mvu.ProgressReader{
72 Reader: reader, // This is the actual reader from exportDockerImage
73 Total: actualSize, // This is the total size of the data to be read
74 ProgressCh: progressCh, // This is the channel used to send progress updates
75 }
76
77 // Create a RequestPayload and populate it
78 reqPayload := common.RequestPayload{
79 Type: "deploy",
80 Payload: common.DeployPayload{
81 Ports: port,
82 TargetDomain: targetDomain,
83 ImageName: imageName,
84 Data: progressReader,
85 },
86 }
87
88 go func() {
89 resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")
90 if err != nil {
91 errCh <- fmt.Errorf("error sending HTTP request: %w", err)
92 return
93 }
94
95 // Check the response
96 targetDomain := string(resp.Body)
97 targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace
98 targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes
99 // Close the progress channel after the upload is complete
100 // Determine if the target is HTTPS or HTTP
101 isHTTPS := strings.HasPrefix(targetDomain, "https://")
102
103 // Initialize HTTP client
104 client := &http.Client{
105 Timeout: 5 * time.Second,
106 }
107
108 // Only set TLS config if target is HTTPS
109 if isHTTPS {
110 client.Transport = &http.Transport{
111 TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated
112 }
113 }
114
115 // Run the TUI program
116 finalModel, err := mvu.RunDeploymentTUI(client, imageName, targetDomain, port)
117 if err != nil {
118 errCh <- fmt.Errorf("error running deployment TUI: %w", err)
119 return
120 }
121
122 // Check if the deployment was successful
123 if !finalModel.DeploymentDone {
124 errCh <- fmt.Errorf("deployment failed")
125 return
126 }
127
128 // Print the final message
129 color.Blue("Deployment successful!")
130 fmt.Println("Your application is now available at:", targetDomain)
131 close(progressCh)
132 }()
133 // Run the progress bar TUI
134 m, err := mvu.RunProgressBarTUI(progressCh)
135 if err != nil {
136 errCh <- fmt.Errorf("error running progress bar TUI: %w", err)
137 return
138 }
139 done := false
140
141 for !done {
142 select {
143 case err := <-errCh:
144 if err != nil {
145 fmt.Println(err)
146 return
147 }
148 case <-time.After(1 * time.Second):
149 // Check if the progress bar is done
150 if m.Done {
151 done = true
152 }
153 }
154 }
155
156 // Close the reader
157 err = progressReader.Close()
158 if err != nil {
159 fmt.Println("Error closing reader:", err)
160 return
161 }
162
163 m.Done = true
164
165 },
166 }
167
168 // Add flags
169 deployCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container")
170 deployCmd.Flags().StringVarP(&targetDomain, "target", "t", "", "Target domain for Traefik")
171
172 return deployCmd
173}
174
175// export the docker image to a reader and return its true size
176func exportDockerImage(imageName string) (io.ReadCloser, int64, error) {
177 imageID, err := docker.GetImageID(imageName)
178 if err != nil {
179 return nil, 0, fmt.Errorf("error getting image ID: %w", err)
180 }
181
182 actualSize, err := docker.GetImageSizeFromReader(imageID)
183 if err != nil {
184 return nil, 0, fmt.Errorf("error getting image size: %w", err)
185 }
186
187 reader, err := docker.ExportImageFromEngine(imageID)
188 if err != nil {
189 return nil, 0, fmt.Errorf("error exporting image: %w", err)
190 }
191
192 // Return the wrapped reader.
193 return reader, actualSize, nil
194}