detached dbdc5cd

Complete Push command feature

- Implement backend flag parsing logic to deploy image to a specific domain
- Introduce `ImageID` field in `PushPayload` struct
- Improve error handling in `PostPush` method with `sendJsonError`
- Add response verification and domain availability check in `push.go`
- Wait & check url availability in final step of push command
commit
dbdc5cd8afe1302911cc0ebda154f1d1f986019b
author
bnema · · 2023-11-01 08:37 UTC
parent
0113c40
signature
unsigned
6 files · +153 -57
M internal/cli/cmd/push.go +48 -5
@@ -1,9 +1,13 @@
11 package cmd
22
33 import (
4+ "crypto/tls"
45 "fmt"
6+ "net/http"
57 "os"
68 "regexp"
9+ "strings"
10+ "time"
711
812 "github.com/bnema/gordon/internal/cli"
913 "github.com/bnema/gordon/internal/cli/handler"
@@ -82,12 +86,51 @@ func NewPushCommand(a *cli.App) *cobra.Command {
8286 // Stop the progress bar
8387 bar.Finish()
8488
85- if resp.StatusCode != 200 {
86- fmt.Println("Unexpected status code:", resp.StatusCode)
87- return
88- }
89+ // Check the response
90+ targetDomain := string(resp.Body)
91+ targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace
92+ targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes
93+
94+ // Notify user
95+ fmt.Println("Wait while Traefik is setting up the domain and certificate...")
96+ progressIndicator := ""
97+ // Check URL availability
98+ for {
99+ client := &http.Client{
100+ Timeout: time.Second * 20,
101+ Transport: &http.Transport{
102+ TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated
103+ },
104+ }
105+
106+ // Making GET request to the target domain
107+ resp, err := client.Get(targetDomain)
108+ if err != nil {
109+ // we do nothing here, we just wait for the domain to be available
110+
111+ } else {
112+ fmt.Println("Domain is available at:", targetDomain)
113+ break
114+ }
89115
90- fmt.Println(resp.Body)
116+ // Close the response body, if non-nil
117+ if resp != nil {
118+ resp.Body.Close()
119+ }
120+
121+ // Update and print the progress indicator
122+ progressIndicator += "."
123+
124+ // Limit the progress indicator to 5 dots
125+ if len(progressIndicator) > 5 {
126+ progressIndicator = ""
127+ }
128+
129+ fmt.Printf("\r%s", progressIndicator)
130+
131+ // Wait for 1 secondsbefore next check
132+ time.Sleep(time.Second)
133+ }
91134 },
92135 }
93136
@@ -1,9 +1,13 @@
1 package cmd1 package cmd
2 2
3 import (3 import (
4+ "crypto/tls"
4 "fmt"5 "fmt"
6+ "net/http"
5 "os"7 "os"
6 "regexp"8 "regexp"
9+ "strings"
10+ "time"
7 11
8 "github.com/bnema/gordon/internal/cli"12 "github.com/bnema/gordon/internal/cli"
9 "github.com/bnema/gordon/internal/cli/handler"13 "github.com/bnema/gordon/internal/cli/handler"
@@ -82,12 +86,51 @@ func NewPushCommand(a *cli.App) *cobra.Command {
82 // Stop the progress bar86 // Stop the progress bar
83 bar.Finish()87 bar.Finish()
84 88
85- if resp.StatusCode != 200 {89+ // Check the response
86- fmt.Println("Unexpected status code:", resp.StatusCode)90+ targetDomain := string(resp.Body)
87- return91+ targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace
88- }92+ targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes
93+
94+ // Notify user
95+ fmt.Println("Wait while Traefik is setting up the domain and certificate...")
96+ progressIndicator := ""
97+ // Check URL availability
98+ for {
99+ client := &http.Client{
100+ Timeout: time.Second * 20,
101+ Transport: &http.Transport{
102+ TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated
103+ },
104+ }
105+
106+ // Making GET request to the target domain
107+ resp, err := client.Get(targetDomain)
108+ if err != nil {
109+ // we do nothing here, we just wait for the domain to be available
110+
111+ } else {
112+ fmt.Println("Domain is available at:", targetDomain)
113+ break
114+ }
89 115
90- fmt.Println(resp.Body)116+ // Close the response body, if non-nil
117+ if resp != nil {
118+ resp.Body.Close()
119+ }
120+
121+ // Update and print the progress indicator
122+ progressIndicator += "."
123+
124+ // Limit the progress indicator to 5 dots
125+ if len(progressIndicator) > 5 {
126+ progressIndicator = ""
127+ }
128+
129+ fmt.Printf("\r%s", progressIndicator)
130+
131+ // Wait for 1 secondsbefore next check
132+ time.Sleep(time.Second)
133+ }
91 },134 },
92 }135 }
93 136
M internal/common/payload.go +1 -0
@@ -73,5 +73,6 @@ type PushPayload struct {
7373 Ports string `json:"ports"`
7474 TargetDomain string `json:"targetdomain"`
7575 ImageName string `json:"imagename"`
76+ ImageID string `json:"imageid"`
7677 Data io.ReadCloser
7778 }
@@ -73,5 +73,6 @@ type PushPayload struct {
73 Ports string `json:"ports"`73 Ports string `json:"ports"`
74 TargetDomain string `json:"targetdomain"`74 TargetDomain string `json:"targetdomain"`
75 ImageName string `json:"imagename"`75 ImageName string `json:"imagename"`
76+ ImageID string `json:"imageid"`
76 Data io.ReadCloser77 Data io.ReadCloser
77 }78 }
M internal/httpserve/handler/clientendpoins.go +34 -17
@@ -9,6 +9,7 @@ import (
99
1010 "github.com/bnema/gordon/internal/common"
1111 "github.com/bnema/gordon/internal/server"
12+ "github.com/bnema/gordon/internal/templating/cmdparams"
1213 "github.com/bnema/gordon/pkg/docker"
1314 "github.com/bnema/gordon/pkg/store"
1415 "github.com/labstack/echo/v4"
@@ -85,19 +86,19 @@ func PostPush(c echo.Context, a *server.App) error {
8586 // Save the image tar in the storage
8687 imageFilePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
8788 if err != nil {
88- return err
89+ return sendJsonError(c, err)
8990 }
9091
9192 // Import the tar in docker
9293 err = docker.ImportImageToEngine(imageFilePath)
9394 if err != nil {
94- return err
95+ return sendJsonError(c, err)
9596 }
9697
9798 // Remove the image from the storage
9899 err = store.RemoveFromStorage()
99100 if err != nil {
100- return err
101+ return sendJsonError(c, err)
101102 }
102103
103104 // append localhost to the image name since it's a manually imported image and not from a registry
@@ -106,23 +107,39 @@ func PostPush(c echo.Context, a *server.App) error {
106107 // Get the image ID
107108 imageID, err := docker.GetImageID(payload.ImageName)
108109 if err != nil {
109- return err
110+ return sendJsonError(c, err)
110111 }
111112
112- // Print the imageID short version
113- fmt.Println("Image ID:", imageID[:12])
113+ // Update the payload with the image ID
114+ payload.ImageID = imageID
114115
115- // // Create the container with traefik labels
116- // err = addTraefikLabels(payload)
117- // if err != nil {
118- // return err
119- // }
116+ // Create the container using cmdparams.FromPayloadStructToCmdParams
117+ params, err := cmdparams.FromPayloadStructToCmdParams(payload, a)
118+ if err != nil {
119+ return sendJsonError(c, err)
120+ }
120121
121- // // Start the container
122- // err = docker.StartContainer(imageID)
123- // if err != nil {
124- // return err
125- // }
122+ // Create the container
123+ containerID, err := docker.CreateContainer(params)
124+ if err != nil {
125+ return sendJsonError(c, err)
126+ }
127+
128+ // Start the container
129+ err = docker.StartContainer(containerID)
130+ if err != nil {
131+ return sendJsonError(c, err)
132+ }
126133
127- return c.JSON(http.StatusOK, "Image pushed successfully")
134+ // Remove the container if it fails to start
135+ defer func() {
136+ if err != nil {
137+ docker.RemoveContainer(containerID)
138+ c.JSON(http.StatusInternalServerError, sendJsonError(c, err))
139+ return
140+ }
141+ }()
142+
143+ // If we arrive here, send back payload.TargetDomain so the client can test it
144+ return c.JSON(http.StatusOK, payload.TargetDomain)
128145 }
@@ -9,6 +9,7 @@ import (
9 9
10 "github.com/bnema/gordon/internal/common"10 "github.com/bnema/gordon/internal/common"
11 "github.com/bnema/gordon/internal/server"11 "github.com/bnema/gordon/internal/server"
12+ "github.com/bnema/gordon/internal/templating/cmdparams"
12 "github.com/bnema/gordon/pkg/docker"13 "github.com/bnema/gordon/pkg/docker"
13 "github.com/bnema/gordon/pkg/store"14 "github.com/bnema/gordon/pkg/store"
14 "github.com/labstack/echo/v4"15 "github.com/labstack/echo/v4"
@@ -85,19 +86,19 @@ func PostPush(c echo.Context, a *server.App) error {
85 // Save the image tar in the storage86 // Save the image tar in the storage
86 imageFilePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)87 imageFilePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
87 if err != nil {88 if err != nil {
88- return err89+ return sendJsonError(c, err)
89 }90 }
90 91
91 // Import the tar in docker92 // Import the tar in docker
92 err = docker.ImportImageToEngine(imageFilePath)93 err = docker.ImportImageToEngine(imageFilePath)
93 if err != nil {94 if err != nil {
94- return err95+ return sendJsonError(c, err)
95 }96 }
96 97
97 // Remove the image from the storage98 // Remove the image from the storage
98 err = store.RemoveFromStorage()99 err = store.RemoveFromStorage()
99 if err != nil {100 if err != nil {
100- return err101+ return sendJsonError(c, err)
101 }102 }
102 103
103 // append localhost to the image name since it's a manually imported image and not from a registry104 // append localhost to the image name since it's a manually imported image and not from a registry
@@ -106,23 +107,39 @@ func PostPush(c echo.Context, a *server.App) error {
106 // Get the image ID107 // Get the image ID
107 imageID, err := docker.GetImageID(payload.ImageName)108 imageID, err := docker.GetImageID(payload.ImageName)
108 if err != nil {109 if err != nil {
109- return err110+ return sendJsonError(c, err)
110 }111 }
111 112
112- // Print the imageID short version113+ // Update the payload with the image ID
113- fmt.Println("Image ID:", imageID[:12])114+ payload.ImageID = imageID
114 115
115- // // Create the container with traefik labels116+ // Create the container using cmdparams.FromPayloadStructToCmdParams
116- // err = addTraefikLabels(payload)117+ params, err := cmdparams.FromPayloadStructToCmdParams(payload, a)
117- // if err != nil {118+ if err != nil {
118- // return err119+ return sendJsonError(c, err)
119- // }120+ }
120 121
121- // // Start the container122+ // Create the container
122- // err = docker.StartContainer(imageID)123+ containerID, err := docker.CreateContainer(params)
123- // if err != nil {124+ if err != nil {
124- // return err125+ return sendJsonError(c, err)
125- // }126+ }
127+
128+ // Start the container
129+ err = docker.StartContainer(containerID)
130+ if err != nil {
131+ return sendJsonError(c, err)
132+ }
126 133
127- return c.JSON(http.StatusOK, "Image pushed successfully")134+ // Remove the container if it fails to start
135+ defer func() {
136+ if err != nil {
137+ docker.RemoveContainer(containerID)
138+ c.JSON(http.StatusInternalServerError, sendJsonError(c, err))
139+ return
140+ }
141+ }()
142+
143+ // If we arrive here, send back payload.TargetDomain so the client can test it
144+ return c.JSON(http.StatusOK, payload.TargetDomain)
128 }145 }
M internal/httpserve/handler/senderror.go +4 -0
@@ -15,3 +15,7 @@ func sendError(c echo.Context, err error) error {
1515 }
1616 return c.HTML(http.StatusInternalServerError, sanitizedHTML)
1717 }
18+
19+func sendJsonError(c echo.Context, err error) error {
20+ return c.JSON(http.StatusInternalServerError, err.Error())
21+}
@@ -15,3 +15,7 @@ func sendError(c echo.Context, err error) error {
15 }15 }
16 return c.HTML(http.StatusInternalServerError, sanitizedHTML)16 return c.HTML(http.StatusInternalServerError, sanitizedHTML)
17 }17 }
18+
19+func sendJsonError(c echo.Context, err error) error {
20+ return c.JSON(http.StatusInternalServerError, err.Error())
21+}
D internal/templating/cmdparams/_fromcli.go +0 -35
@@ -1,35 +0,0 @@
1-package cmdparams
2-
3-import (
4- "io"
5-
6- "github.com/bnema/gordon/internal/common"
7- "github.com/bnema/gordon/pkg/docker"
8-)
9-
10-type PushPayload struct {
11- Ports string `json:"ports"`
12- TargetDomain string `json:"targetdomain"`
13- ImageName string `json:"imagename"`
14- Data io.ReadCloser
15-}
16-
17-// FromYAMLStructToCmdParams converts a YAMLContainerParams struct to a ContainerCommandParams struct
18-func FromPayloadStructToCmdParams(ppl *common.PushPayload) (docker.ContainerCommandParams, error) {
19-
20- params := docker.ContainerCommandParams{
21- IsSSL: ppl.IsSSL,
22- ContainerName: ppl.ContainerName,
23- ServiceName: ppl.ServiceName,
24- Domain: ppl.Domain,
25- ImageName: ppl.ImageName,
26- ImageID: ppl.ImageID,
27- Restart: ppl.Restart,
28- Volumes: ppl.Volumes,
29- Environment: ppl.Environment,
30- Network: ppl.Network,
31- PortMappings: ppl.PortMappings,
32- }
33-
34- return params, nil
35-}
@@ -1,35 +0,0 @@
1-package cmdparams
2-
3-import (
4- "io"
5-
6- "github.com/bnema/gordon/internal/common"
7- "github.com/bnema/gordon/pkg/docker"
8-)
9-
10-type PushPayload struct {
11- Ports string `json:"ports"`
12- TargetDomain string `json:"targetdomain"`
13- ImageName string `json:"imagename"`
14- Data io.ReadCloser
15-}
16-
17-// FromYAMLStructToCmdParams converts a YAMLContainerParams struct to a ContainerCommandParams struct
18-func FromPayloadStructToCmdParams(ppl *common.PushPayload) (docker.ContainerCommandParams, error) {
19-
20- params := docker.ContainerCommandParams{
21- IsSSL: ppl.IsSSL,
22- ContainerName: ppl.ContainerName,
23- ServiceName: ppl.ServiceName,
24- Domain: ppl.Domain,
25- ImageName: ppl.ImageName,
26- ImageID: ppl.ImageID,
27- Restart: ppl.Restart,
28- Volumes: ppl.Volumes,
29- Environment: ppl.Environment,
30- Network: ppl.Network,
31- PortMappings: ppl.PortMappings,
32- }
33-
34- return params, nil
35-}
A internal/templating/cmdparams/fromcli.go +66 -0
@@ -0,0 +1,66 @@
1+package cmdparams
2+
3+import (
4+ "fmt"
5+ "net/url"
6+ "strings"
7+
8+ "github.com/bnema/gordon/internal/common"
9+ "github.com/bnema/gordon/internal/server"
10+ "github.com/bnema/gordon/pkg/docker"
11+)
12+
13+// FromYAMLStructToCmdParams converts a YAMLContainerParams struct to a ContainerCommandParams struct
14+func FromPayloadStructToCmdParams(ppl *common.PushPayload, a *server.App) (docker.ContainerCommandParams, error) {
15+
16+ imageID, err := docker.GetImageID(ppl.ImageName)
17+ if err != nil {
18+ return docker.ContainerCommandParams{}, err
19+ }
20+
21+ // volume empty for now
22+ volumeSlice := ParseVolumeSlice("")
23+
24+ environmentSlice := ParseEnvironmentSlice("")
25+
26+ portMappings, err := ParsePortMappingsSlice(ppl.Ports)
27+ if err != nil {
28+ return docker.ContainerCommandParams{}, err
29+ }
30+
31+ // from ppl.TargetDomain, (eg. "https://test.example.com") get container_name (eg. "test"), get container_subdomain (eg. "test"), get container_domain (eg. "example.com") and get container_protocol (eg. "https")
32+ // Parse TargetDomain to get the various components
33+ parsedURL, err := url.Parse(ppl.TargetDomain)
34+ if err != nil {
35+ return docker.ContainerCommandParams{}, err
36+ }
37+
38+ // Extract the subdomain and domain from the host
39+ hostParts := strings.Split(parsedURL.Host, ".")
40+ if len(hostParts) < 2 {
41+ return docker.ContainerCommandParams{}, fmt.Errorf("invalid domain: %s", parsedURL.Host)
42+ }
43+
44+ // Protocol (http/https), Container name, Container subdomain, and Container domain
45+ protocol := parsedURL.Scheme
46+ containerSubdomain := hostParts[0]
47+ containerDomain := strings.Join(hostParts[1:], ".")
48+
49+ params := docker.ContainerCommandParams{
50+ IsSSL: protocol == "https",
51+ ContainerName: containerSubdomain,
52+ ServiceName: containerSubdomain,
53+ Domain: containerDomain,
54+ ImageName: ppl.ImageName,
55+ ImageID: imageID,
56+ Restart: "always",
57+ Volumes: volumeSlice,
58+ Environment: environmentSlice,
59+ Network: a.Config.ContainerEngine.Network,
60+ PortMappings: portMappings,
61+ }
62+
63+ CreateTraefikLabels(&params)
64+
65+ return params, nil
66+}
@@ -0,0 +1,66 @@
1+package cmdparams
2+
3+import (
4+ "fmt"
5+ "net/url"
6+ "strings"
7+
8+ "github.com/bnema/gordon/internal/common"
9+ "github.com/bnema/gordon/internal/server"
10+ "github.com/bnema/gordon/pkg/docker"
11+)
12+
13+// FromYAMLStructToCmdParams converts a YAMLContainerParams struct to a ContainerCommandParams struct
14+func FromPayloadStructToCmdParams(ppl *common.PushPayload, a *server.App) (docker.ContainerCommandParams, error) {
15+
16+ imageID, err := docker.GetImageID(ppl.ImageName)
17+ if err != nil {
18+ return docker.ContainerCommandParams{}, err
19+ }
20+
21+ // volume empty for now
22+ volumeSlice := ParseVolumeSlice("")
23+
24+ environmentSlice := ParseEnvironmentSlice("")
25+
26+ portMappings, err := ParsePortMappingsSlice(ppl.Ports)
27+ if err != nil {
28+ return docker.ContainerCommandParams{}, err
29+ }
30+
31+ // from ppl.TargetDomain, (eg. "https://test.example.com") get container_name (eg. "test"), get container_subdomain (eg. "test"), get container_domain (eg. "example.com") and get container_protocol (eg. "https")
32+ // Parse TargetDomain to get the various components
33+ parsedURL, err := url.Parse(ppl.TargetDomain)
34+ if err != nil {
35+ return docker.ContainerCommandParams{}, err
36+ }
37+
38+ // Extract the subdomain and domain from the host
39+ hostParts := strings.Split(parsedURL.Host, ".")
40+ if len(hostParts) < 2 {
41+ return docker.ContainerCommandParams{}, fmt.Errorf("invalid domain: %s", parsedURL.Host)
42+ }
43+
44+ // Protocol (http/https), Container name, Container subdomain, and Container domain
45+ protocol := parsedURL.Scheme
46+ containerSubdomain := hostParts[0]
47+ containerDomain := strings.Join(hostParts[1:], ".")
48+
49+ params := docker.ContainerCommandParams{
50+ IsSSL: protocol == "https",
51+ ContainerName: containerSubdomain,
52+ ServiceName: containerSubdomain,
53+ Domain: containerDomain,
54+ ImageName: ppl.ImageName,
55+ ImageID: imageID,
56+ Restart: "always",
57+ Volumes: volumeSlice,
58+ Environment: environmentSlice,
59+ Network: a.Config.ContainerEngine.Network,
60+ PortMappings: portMappings,
61+ }
62+
63+ CreateTraefikLabels(&params)
64+
65+ return params, nil
66+}

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