detached 13776a4

Backend: Enhance container deployment workflow with existing container handling

- Add container stop and remove endpoints to API
- Implement retry logic for deployment when container already exists
- Extend DeployResponse to include container name
- Add new response types for Stop, Remove, and Start operations
- Improve error handling and logging for deployment conflicts
- Add container name to Stop and Start payloads
- Refactor deployImage function to handle container conflicts
- Update logging format to use structured logging
- Add helper function to get container name after creation
- Add new API routes for container management operations (/stop, /remove)
commit
13776a46e80c2c100eb7924627a2e23f5ad52165
author
Brice · · 2024-10-31 22:02 UTC
committer
Brice · · 2024-10-31 20:11 UTC
parent
15708ae
signature
unsigned
5 files · +275 -61
M internal/cli/cmd/deploy.go +56 -44
@@ -115,57 +115,69 @@ func exportDockerImage(imageName string) (io.ReadCloser, int64, error) {
115115 }
116116
117117 func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain string) error {
118+ log.Info("Attempting to deploy...",
119+ "image", imageName,
120+ "port", port,
121+ "target", targetDomain,
122+ )
123+
124+ for {
125+ reqPayload := common.RequestPayload{
126+ Type: "deploy",
127+ Payload: common.DeployPayload{
128+ Port: port,
129+ TargetDomain: targetDomain,
130+ ImageName: imageName,
131+ Data: io.NopCloser(reader),
132+ },
133+ }
118134
119- log.Info("Attempting to deploy...", "image", imageName, "port", port, "target", targetDomain)
120-
121- reqPayload := common.RequestPayload{
122- Type: "deploy",
123- Payload: common.DeployPayload{
124- Port: port,
125- TargetDomain: targetDomain,
126- ImageName: imageName,
127- Data: io.NopCloser(reader),
128- },
129- }
130-
131- resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/deploy")
132- if err != nil {
133- var deployErr *common.DeploymentError
134- if errors.As(err, &deployErr) {
135- // Try to parse the raw response to get the container ID
136- var deployResponse common.DeployResponse
137- if jsonErr := json.Unmarshal([]byte(deployErr.RawResponse), &deployResponse); jsonErr == nil {
138- log.Error("Deployment failed",
139- "error", deployErr.Message,
140- "status_code", deployErr.StatusCode,
141- "container_id", deployResponse.ContainerID,
142- )
143- } else {
144- // If parsing fails, just log the error without the container ID
145- log.Error("Deployment failed", "error", deployErr.Message, "status_code", deployErr.StatusCode)
135+ resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/deploy")
136+ if err != nil {
137+ var deployErr *common.DeploymentError
138+ if errors.As(err, &deployErr) {
139+ var deployResponse common.DeployResponse
140+ if jsonErr := json.Unmarshal([]byte(deployErr.RawResponse), &deployResponse); jsonErr == nil {
141+ // Handle existing container case
142+ if deployErr.StatusCode == http.StatusConflict && deployResponse.ContainerID != "" {
143+ if err := handler.HandleExistingContainer(a, &deployResponse); err != nil {
144+ if strings.Contains(err.Error(), "cancelled by user") {
145+ log.Warn("Deployment cancelled by user")
146+ } else {
147+ log.Error("Failed to handle existing container", "error", err)
148+ }
149+ return nil
150+ }
151+ log.Info("Retrying deployment...")
152+ continue // Retry deployment
153+ }
154+
155+ log.Error("Deployment failed",
156+ "error", deployErr.Message,
157+ "status_code", deployErr.StatusCode,
158+ "container_id", deployResponse.ContainerID,
159+ )
160+ }
146161 }
147- // Return nil to prevent further error logging
148162 return nil
149163 }
150- log.Error("Error sending HTTP request", "error", err)
151- return nil
152- }
153164
154- var deployResponse common.DeployResponse
155- if err := json.Unmarshal(resp.Body, &deployResponse); err != nil {
156- log.Error("Error parsing response", "error", err, "response", string(resp.Body))
157- return nil
158- }
165+ var deployResponse common.DeployResponse
166+ if err := json.Unmarshal(resp.Body, &deployResponse); err != nil {
167+ log.Error("Error parsing response", "error", err, "response", string(resp.Body))
168+ return nil
169+ }
159170
160- if !deployResponse.Success {
161- log.Error("Deployment failed",
162- "message", deployResponse.Message,
163- "container_id", deployResponse.ContainerID,
164- )
165- return nil
166- }
171+ if !deployResponse.Success {
172+ log.Error("Deployment failed",
173+ "message", deployResponse.Message,
174+ "container_id", deployResponse.ContainerID,
175+ )
176+ return nil
177+ }
167178
168- return waitForDeployment(deployResponse.Domain, deployResponse.ContainerID)
179+ return waitForDeployment(deployResponse.Domain, deployResponse.ContainerID)
180+ }
169181 }
170182
171183 func waitForDeployment(domain string, containerID string) error {
@@ -115,57 +115,69 @@ func exportDockerImage(imageName string) (io.ReadCloser, int64, error) {
115 }115 }
116 116
117 func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain string) error {117 func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain string) error {
118+ log.Info("Attempting to deploy...",
119+ "image", imageName,
120+ "port", port,
121+ "target", targetDomain,
122+ )
123+
124+ for {
125+ reqPayload := common.RequestPayload{
126+ Type: "deploy",
127+ Payload: common.DeployPayload{
128+ Port: port,
129+ TargetDomain: targetDomain,
130+ ImageName: imageName,
131+ Data: io.NopCloser(reader),
132+ },
133+ }
118 134
119- log.Info("Attempting to deploy...", "image", imageName, "port", port, "target", targetDomain)135+ resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/deploy")
120-136+ if err != nil {
121- reqPayload := common.RequestPayload{137+ var deployErr *common.DeploymentError
122- Type: "deploy",138+ if errors.As(err, &deployErr) {
123- Payload: common.DeployPayload{139+ var deployResponse common.DeployResponse
124- Port: port,140+ if jsonErr := json.Unmarshal([]byte(deployErr.RawResponse), &deployResponse); jsonErr == nil {
125- TargetDomain: targetDomain,141+ // Handle existing container case
126- ImageName: imageName,142+ if deployErr.StatusCode == http.StatusConflict && deployResponse.ContainerID != "" {
127- Data: io.NopCloser(reader),143+ if err := handler.HandleExistingContainer(a, &deployResponse); err != nil {
128- },144+ if strings.Contains(err.Error(), "cancelled by user") {
129- }145+ log.Warn("Deployment cancelled by user")
130-146+ } else {
131- resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/deploy")147+ log.Error("Failed to handle existing container", "error", err)
132- if err != nil {148+ }
133- var deployErr *common.DeploymentError149+ return nil
134- if errors.As(err, &deployErr) {150+ }
135- // Try to parse the raw response to get the container ID151+ log.Info("Retrying deployment...")
136- var deployResponse common.DeployResponse152+ continue // Retry deployment
137- if jsonErr := json.Unmarshal([]byte(deployErr.RawResponse), &deployResponse); jsonErr == nil {153+ }
138- log.Error("Deployment failed",154+
139- "error", deployErr.Message,155+ log.Error("Deployment failed",
140- "status_code", deployErr.StatusCode,156+ "error", deployErr.Message,
141- "container_id", deployResponse.ContainerID,157+ "status_code", deployErr.StatusCode,
142- )158+ "container_id", deployResponse.ContainerID,
143- } else {159+ )
144- // If parsing fails, just log the error without the container ID160+ }
145- log.Error("Deployment failed", "error", deployErr.Message, "status_code", deployErr.StatusCode)
146 }161 }
147- // Return nil to prevent further error logging
148 return nil162 return nil
149 }163 }
150- log.Error("Error sending HTTP request", "error", err)
151- return nil
152- }
153 164
154- var deployResponse common.DeployResponse165+ var deployResponse common.DeployResponse
155- if err := json.Unmarshal(resp.Body, &deployResponse); err != nil {166+ if err := json.Unmarshal(resp.Body, &deployResponse); err != nil {
156- log.Error("Error parsing response", "error", err, "response", string(resp.Body))167+ log.Error("Error parsing response", "error", err, "response", string(resp.Body))
157- return nil168+ return nil
158- }169+ }
159 170
160- if !deployResponse.Success {171+ if !deployResponse.Success {
161- log.Error("Deployment failed",172+ log.Error("Deployment failed",
162- "message", deployResponse.Message,173+ "message", deployResponse.Message,
163- "container_id", deployResponse.ContainerID,174+ "container_id", deployResponse.ContainerID,
164- )175+ )
165- return nil176+ return nil
166- }177+ }
167 178
168- return waitForDeployment(deployResponse.Domain, deployResponse.ContainerID)179+ return waitForDeployment(deployResponse.Domain, deployResponse.ContainerID)
180+ }
169 }181 }
170 182
171 func waitForDeployment(domain string, containerID string) error {183 func waitForDeployment(domain string, containerID string) error {
A internal/cli/handler/deploy_survey.go +102 -0
@@ -0,0 +1,102 @@
1+// gordon/internal/cli/handler/deploy_survey.go
2+
3+package handler
4+
5+import (
6+ "encoding/json"
7+ "fmt"
8+
9+ "github.com/AlecAivazis/survey/v2"
10+ "github.com/bnema/gordon/internal/cli"
11+ "github.com/bnema/gordon/internal/common"
12+ "github.com/charmbracelet/log"
13+)
14+
15+func HandleExistingContainer(a *cli.App, deployResponse *common.DeployResponse) error {
16+ containerName := deployResponse.ContainerName
17+ shortID := deployResponse.ContainerID[:12]
18+
19+ var proceed bool
20+ prompt := &survey.Confirm{
21+ Message: fmt.Sprintf("Container '%s' (ID: %s) is already running. Stop and remove it?", containerName, shortID),
22+ Default: true,
23+ }
24+
25+ if err := survey.AskOne(prompt, &proceed); err != nil {
26+ return fmt.Errorf("survey failed: %w", err)
27+ }
28+
29+ if !proceed {
30+ return fmt.Errorf("operation cancelled by user")
31+ }
32+
33+ log.Info("Stopping container...", "name", containerName, "id", shortID)
34+
35+ // Stop the container
36+ if err := stopContainer(a, deployResponse.ContainerID); err != nil {
37+ return fmt.Errorf("failed to stop container '%s': %w", containerName, err)
38+ }
39+
40+ log.Info("Removing container...", "name", containerName, "id", shortID)
41+
42+ // Remove the container
43+ if err := removeContainer(a, deployResponse.ContainerID); err != nil {
44+ return fmt.Errorf("failed to remove container '%s': %w", containerName, err)
45+ }
46+
47+ log.Info("Container stopped and removed successfully",
48+ "name", containerName,
49+ "id", shortID,
50+ )
51+ return nil
52+}
53+
54+func removeContainer(a *cli.App, containerID string) error {
55+ reqPayload := common.RequestPayload{
56+ Type: "remove",
57+ Payload: common.RemovePayload{
58+ ContainerID: containerID,
59+ },
60+ }
61+
62+ resp, err := SendHTTPRequest(a, &reqPayload, "POST", "/remove")
63+ if err != nil {
64+ return err
65+ }
66+
67+ var removeResponse common.RemoveResponse
68+ if err := json.Unmarshal(resp.Body, &removeResponse); err != nil {
69+ return fmt.Errorf("failed to parse remove response: %w", err)
70+ }
71+
72+ if !removeResponse.Success {
73+ return fmt.Errorf("failed to remove container: %s", removeResponse.Message)
74+ }
75+
76+ return nil
77+}
78+
79+func stopContainer(a *cli.App, containerID string) error {
80+ reqPayload := common.RequestPayload{
81+ Type: "stop",
82+ Payload: common.StopPayload{
83+ ContainerID: containerID,
84+ },
85+ }
86+
87+ resp, err := SendHTTPRequest(a, &reqPayload, "POST", "/stop")
88+ if err != nil {
89+ return err
90+ }
91+
92+ var stopResponse common.StopResponse
93+ if err := json.Unmarshal(resp.Body, &stopResponse); err != nil {
94+ return fmt.Errorf("failed to parse stop response: %w", err)
95+ }
96+
97+ if !stopResponse.Success {
98+ return fmt.Errorf("failed to stop container: %s", stopResponse.Message)
99+ }
100+
101+ return nil
102+}
@@ -0,0 +1,102 @@
1+// gordon/internal/cli/handler/deploy_survey.go
2+
3+package handler
4+
5+import (
6+ "encoding/json"
7+ "fmt"
8+
9+ "github.com/AlecAivazis/survey/v2"
10+ "github.com/bnema/gordon/internal/cli"
11+ "github.com/bnema/gordon/internal/common"
12+ "github.com/charmbracelet/log"
13+)
14+
15+func HandleExistingContainer(a *cli.App, deployResponse *common.DeployResponse) error {
16+ containerName := deployResponse.ContainerName
17+ shortID := deployResponse.ContainerID[:12]
18+
19+ var proceed bool
20+ prompt := &survey.Confirm{
21+ Message: fmt.Sprintf("Container '%s' (ID: %s) is already running. Stop and remove it?", containerName, shortID),
22+ Default: true,
23+ }
24+
25+ if err := survey.AskOne(prompt, &proceed); err != nil {
26+ return fmt.Errorf("survey failed: %w", err)
27+ }
28+
29+ if !proceed {
30+ return fmt.Errorf("operation cancelled by user")
31+ }
32+
33+ log.Info("Stopping container...", "name", containerName, "id", shortID)
34+
35+ // Stop the container
36+ if err := stopContainer(a, deployResponse.ContainerID); err != nil {
37+ return fmt.Errorf("failed to stop container '%s': %w", containerName, err)
38+ }
39+
40+ log.Info("Removing container...", "name", containerName, "id", shortID)
41+
42+ // Remove the container
43+ if err := removeContainer(a, deployResponse.ContainerID); err != nil {
44+ return fmt.Errorf("failed to remove container '%s': %w", containerName, err)
45+ }
46+
47+ log.Info("Container stopped and removed successfully",
48+ "name", containerName,
49+ "id", shortID,
50+ )
51+ return nil
52+}
53+
54+func removeContainer(a *cli.App, containerID string) error {
55+ reqPayload := common.RequestPayload{
56+ Type: "remove",
57+ Payload: common.RemovePayload{
58+ ContainerID: containerID,
59+ },
60+ }
61+
62+ resp, err := SendHTTPRequest(a, &reqPayload, "POST", "/remove")
63+ if err != nil {
64+ return err
65+ }
66+
67+ var removeResponse common.RemoveResponse
68+ if err := json.Unmarshal(resp.Body, &removeResponse); err != nil {
69+ return fmt.Errorf("failed to parse remove response: %w", err)
70+ }
71+
72+ if !removeResponse.Success {
73+ return fmt.Errorf("failed to remove container: %s", removeResponse.Message)
74+ }
75+
76+ return nil
77+}
78+
79+func stopContainer(a *cli.App, containerID string) error {
80+ reqPayload := common.RequestPayload{
81+ Type: "stop",
82+ Payload: common.StopPayload{
83+ ContainerID: containerID,
84+ },
85+ }
86+
87+ resp, err := SendHTTPRequest(a, &reqPayload, "POST", "/stop")
88+ if err != nil {
89+ return err
90+ }
91+
92+ var stopResponse common.StopResponse
93+ if err := json.Unmarshal(resp.Body, &stopResponse); err != nil {
94+ return fmt.Errorf("failed to parse stop response: %w", err)
95+ }
96+
97+ if !stopResponse.Success {
98+ return fmt.Errorf("failed to stop container: %s", stopResponse.Message)
99+ }
100+
101+ return nil
102+}
M internal/common/payload.go +24 -6
@@ -19,10 +19,11 @@ type Payload interface {
1919
2020 // DeployResponse represents the response structure for deployment requests
2121 type DeployResponse struct {
22- Success bool `json:"success"`
23- Message string `json:"message"`
24- Domain string `json:"domain,omitempty"`
25- ContainerID string `json:"container_id,omitempty"`
22+ Success bool `json:"success"`
23+ Message string `json:"message"`
24+ Domain string `json:"domain"`
25+ ContainerID string `json:"container_id"`
26+ ContainerName string `json:"container_name"`
2627 }
2728
2829 // PushResponse represents the response structure for push requests
@@ -32,6 +33,21 @@ type PushResponse struct {
3233 CreateContainerURL string `json:"create_container_url,omitempty"`
3334 }
3435
36+type StopResponse struct {
37+ Success bool `json:"success"`
38+ Message string `json:"message"`
39+}
40+
41+type RemoveResponse struct {
42+ Success bool `json:"success"`
43+ Message string `json:"message"`
44+}
45+
46+type StartResponse struct {
47+ Success bool `json:"success"`
48+ Message string `json:"message"`
49+}
50+
3551 // DeploymentError represents an error that occurred during deployment
3652 type DeploymentError struct {
3753 StatusCode int `json:"status"`
@@ -148,7 +164,8 @@ func NewPingPayload(data map[string]interface{}) (PingPayload, error) {
148164
149165 // StopPayload represents the payload for stop requests
150166 type StopPayload struct {
151- ContainerID string `json:"container_id"`
167+ ContainerID string `json:"container_id"`
168+ ContainerName string `json:"container_name"`
152169 }
153170
154171 // GetType returns the type of the StopPayload
@@ -158,7 +175,8 @@ func (p StopPayload) GetType() string {
158175
159176 // StartPayload represents the payload for start requests
160177 type StartPayload struct {
161- ContainerID string `json:"container_id"`
178+ ContainerID string `json:"container_id"`
179+ ContainerName string `json:"container_name"`
162180 }
163181
164182 // GetType returns the type of the StartPayload
@@ -19,10 +19,11 @@ type Payload interface {
19 19
20 // DeployResponse represents the response structure for deployment requests20 // DeployResponse represents the response structure for deployment requests
21 type DeployResponse struct {21 type DeployResponse struct {
22- Success bool `json:"success"`22+ Success bool `json:"success"`
23- Message string `json:"message"`23+ Message string `json:"message"`
24- Domain string `json:"domain,omitempty"`24+ Domain string `json:"domain"`
25- ContainerID string `json:"container_id,omitempty"`25+ ContainerID string `json:"container_id"`
26+ ContainerName string `json:"container_name"`
26 }27 }
27 28
28 // PushResponse represents the response structure for push requests29 // PushResponse represents the response structure for push requests
@@ -32,6 +33,21 @@ type PushResponse struct {
32 CreateContainerURL string `json:"create_container_url,omitempty"`33 CreateContainerURL string `json:"create_container_url,omitempty"`
33 }34 }
34 35
36+type StopResponse struct {
37+ Success bool `json:"success"`
38+ Message string `json:"message"`
39+}
40+
41+type RemoveResponse struct {
42+ Success bool `json:"success"`
43+ Message string `json:"message"`
44+}
45+
46+type StartResponse struct {
47+ Success bool `json:"success"`
48+ Message string `json:"message"`
49+}
50+
35 // DeploymentError represents an error that occurred during deployment51 // DeploymentError represents an error that occurred during deployment
36 type DeploymentError struct {52 type DeploymentError struct {
37 StatusCode int `json:"status"`53 StatusCode int `json:"status"`
@@ -148,7 +164,8 @@ func NewPingPayload(data map[string]interface{}) (PingPayload, error) {
148 164
149 // StopPayload represents the payload for stop requests165 // StopPayload represents the payload for stop requests
150 type StopPayload struct {166 type StopPayload struct {
151- ContainerID string `json:"container_id"`167+ ContainerID string `json:"container_id"`
168+ ContainerName string `json:"container_name"`
152 }169 }
153 170
154 // GetType returns the type of the StopPayload171 // GetType returns the type of the StopPayload
@@ -158,7 +175,8 @@ func (p StopPayload) GetType() string {
158 175
159 // StartPayload represents the payload for start requests176 // StartPayload represents the payload for start requests
160 type StartPayload struct {177 type StartPayload struct {
161- ContainerID string `json:"container_id"`178+ ContainerID string `json:"container_id"`
179+ ContainerName string `json:"container_name"`
162 }180 }
163 181
164 // GetType returns the type of the StartPayload182 // GetType returns the type of the StartPayload
M internal/httpserve/handlers/cli_endpoints.go +87 -11
@@ -24,6 +24,8 @@ type InfoResponse struct {
2424
2525 type DeployResponse = common.DeployResponse
2626 type PushResponse = common.PushResponse
27+type StopResponse = common.StopResponse
28+type RemoveResponse = common.RemoveResponse
2729
2830 // sendJSONResponse is a helper function to send JSON responses
2931 func sendJSONResponse(c echo.Context, statusCode int, response interface{}) error {
@@ -64,6 +66,74 @@ func GetInfos(c echo.Context, a *server.App) error {
6466 return sendJSONResponse(c, http.StatusOK, info)
6567 }
6668
69+// PostContainerStop handles the container stop request
70+func PostContainerStop(c echo.Context, a *server.App) error {
71+ payload := new(common.RequestPayload)
72+ if err := c.Bind(payload); err != nil {
73+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error())
74+ }
75+
76+ if payload.Type != "stop" {
77+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type")
78+ }
79+
80+ stopPayload, ok := payload.Payload.(common.StopPayload)
81+ if !ok {
82+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure")
83+ }
84+
85+ if stopPayload.ContainerID == "" {
86+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid container ID")
87+ }
88+
89+ err := docker.StopContainer(stopPayload.ContainerID)
90+ if err != nil {
91+ return sendJSONResponse(c, http.StatusInternalServerError, StopResponse{
92+ Success: false,
93+ Message: err.Error(),
94+ })
95+ }
96+
97+ return sendJSONResponse(c, http.StatusOK, StopResponse{
98+ Success: true,
99+ Message: "Container stopped successfully",
100+ })
101+}
102+
103+// PostContainerRemove handles the container remove request
104+func PostContainerRemove(c echo.Context, a *server.App) error {
105+ payload := new(common.RequestPayload)
106+ if err := c.Bind(payload); err != nil {
107+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error())
108+ }
109+
110+ if payload.Type != "remove" {
111+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type")
112+ }
113+
114+ removePayload, ok := payload.Payload.(common.RemovePayload)
115+ if !ok {
116+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure")
117+ }
118+
119+ if removePayload.ContainerID == "" {
120+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid container ID")
121+ }
122+
123+ err := docker.RemoveContainer(removePayload.ContainerID)
124+ if err != nil {
125+ return sendJSONResponse(c, http.StatusInternalServerError, RemoveResponse{
126+ Success: false,
127+ Message: err.Error(),
128+ })
129+ }
130+
131+ return sendJSONResponse(c, http.StatusOK, RemoveResponse{
132+ Success: true,
133+ Message: "Container removed successfully",
134+ })
135+}
136+
67137 // PostPush handles the image push request
68138 func PostPush(c echo.Context, a *server.App) error {
69139 payload, err := validateAndPreparePayload(c)
@@ -190,7 +260,7 @@ func PostDeploy(c echo.Context, a *server.App) error {
190260 })
191261 }
192262
193- containerID, err := createAndStartContainer(a, payload)
263+ containerID, containerName, err := createAndStartContainer(a, payload)
194264 if err != nil {
195265 if strings.Contains(err.Error(), "is already in use") {
196266 log.Warn("Container already exists", "error", err)
@@ -218,10 +288,11 @@ func PostDeploy(c echo.Context, a *server.App) error {
218288 })
219289 }
220290 response := DeployResponse{
221- Success: true,
222- Message: "Deployment successful",
223- Domain: payload.TargetDomain,
224- ContainerID: containerID,
291+ Success: true,
292+ Message: "Deployment successful",
293+ Domain: payload.TargetDomain,
294+ ContainerID: containerID,
295+ ContainerName: containerName,
225296 }
226297
227298 // Debug: Verify containerID in response
@@ -310,28 +381,33 @@ func saveAndImportDeployImage(c echo.Context, a *server.App, payload *common.Dep
310381 }
311382
312383 // createAndStartContainer creates and starts a new container
313-func createAndStartContainer(a *server.App, payload *common.DeployPayload) (string, error) {
384+func createAndStartContainer(a *server.App, payload *common.DeployPayload) (string, string, error) {
314385 params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, payload.ImageID)
315386 if err != nil {
316- return "", fmt.Errorf("failed to create command parameters: %v", err)
387+ return "", "", fmt.Errorf("failed to create command parameters: %v", err)
317388 }
318389
319390 containerID, err := docker.CreateContainer(params)
320391 if err != nil {
321- return "", fmt.Errorf("failed to create container: %v", err)
392+ return "", "", fmt.Errorf("failed to create container: %v", err)
393+ }
394+
395+ containerName, err := docker.GetContainerName(containerID)
396+ if err != nil {
397+ return "", "", fmt.Errorf("failed to get container name: %v", err)
322398 }
323399
324400 err = docker.StartContainer(containerID)
325401 if err != nil {
326402 docker.RemoveContainer(containerID)
327- return "", fmt.Errorf("failed to start container: %v", err)
403+ return "", "", fmt.Errorf("failed to start container: %v", err)
328404 }
329405
330- return containerID, nil
406+ return containerID, containerName, nil
331407 }
332408
333409 // logResponse logs the deployment response
334410 func logResponse(response DeployResponse) {
335411 responseJSON, _ := json.Marshal(response)
336- fmt.Printf("Server response: %s\n", string(responseJSON))
412+ log.Info("Deployment response", string(responseJSON))
337413 }
@@ -24,6 +24,8 @@ type InfoResponse struct {
24 24
25 type DeployResponse = common.DeployResponse25 type DeployResponse = common.DeployResponse
26 type PushResponse = common.PushResponse26 type PushResponse = common.PushResponse
27+type StopResponse = common.StopResponse
28+type RemoveResponse = common.RemoveResponse
27 29
28 // sendJSONResponse is a helper function to send JSON responses30 // sendJSONResponse is a helper function to send JSON responses
29 func sendJSONResponse(c echo.Context, statusCode int, response interface{}) error {31 func sendJSONResponse(c echo.Context, statusCode int, response interface{}) error {
@@ -64,6 +66,74 @@ func GetInfos(c echo.Context, a *server.App) error {
64 return sendJSONResponse(c, http.StatusOK, info)66 return sendJSONResponse(c, http.StatusOK, info)
65 }67 }
66 68
69+// PostContainerStop handles the container stop request
70+func PostContainerStop(c echo.Context, a *server.App) error {
71+ payload := new(common.RequestPayload)
72+ if err := c.Bind(payload); err != nil {
73+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error())
74+ }
75+
76+ if payload.Type != "stop" {
77+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type")
78+ }
79+
80+ stopPayload, ok := payload.Payload.(common.StopPayload)
81+ if !ok {
82+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure")
83+ }
84+
85+ if stopPayload.ContainerID == "" {
86+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid container ID")
87+ }
88+
89+ err := docker.StopContainer(stopPayload.ContainerID)
90+ if err != nil {
91+ return sendJSONResponse(c, http.StatusInternalServerError, StopResponse{
92+ Success: false,
93+ Message: err.Error(),
94+ })
95+ }
96+
97+ return sendJSONResponse(c, http.StatusOK, StopResponse{
98+ Success: true,
99+ Message: "Container stopped successfully",
100+ })
101+}
102+
103+// PostContainerRemove handles the container remove request
104+func PostContainerRemove(c echo.Context, a *server.App) error {
105+ payload := new(common.RequestPayload)
106+ if err := c.Bind(payload); err != nil {
107+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error())
108+ }
109+
110+ if payload.Type != "remove" {
111+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type")
112+ }
113+
114+ removePayload, ok := payload.Payload.(common.RemovePayload)
115+ if !ok {
116+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure")
117+ }
118+
119+ if removePayload.ContainerID == "" {
120+ return sendJSONResponse(c, http.StatusBadRequest, "Invalid container ID")
121+ }
122+
123+ err := docker.RemoveContainer(removePayload.ContainerID)
124+ if err != nil {
125+ return sendJSONResponse(c, http.StatusInternalServerError, RemoveResponse{
126+ Success: false,
127+ Message: err.Error(),
128+ })
129+ }
130+
131+ return sendJSONResponse(c, http.StatusOK, RemoveResponse{
132+ Success: true,
133+ Message: "Container removed successfully",
134+ })
135+}
136+
67 // PostPush handles the image push request137 // PostPush handles the image push request
68 func PostPush(c echo.Context, a *server.App) error {138 func PostPush(c echo.Context, a *server.App) error {
69 payload, err := validateAndPreparePayload(c)139 payload, err := validateAndPreparePayload(c)
@@ -190,7 +260,7 @@ func PostDeploy(c echo.Context, a *server.App) error {
190 })260 })
191 }261 }
192 262
193- containerID, err := createAndStartContainer(a, payload)263+ containerID, containerName, err := createAndStartContainer(a, payload)
194 if err != nil {264 if err != nil {
195 if strings.Contains(err.Error(), "is already in use") {265 if strings.Contains(err.Error(), "is already in use") {
196 log.Warn("Container already exists", "error", err)266 log.Warn("Container already exists", "error", err)
@@ -218,10 +288,11 @@ func PostDeploy(c echo.Context, a *server.App) error {
218 })288 })
219 }289 }
220 response := DeployResponse{290 response := DeployResponse{
221- Success: true,291+ Success: true,
222- Message: "Deployment successful",292+ Message: "Deployment successful",
223- Domain: payload.TargetDomain,293+ Domain: payload.TargetDomain,
224- ContainerID: containerID,294+ ContainerID: containerID,
295+ ContainerName: containerName,
225 }296 }
226 297
227 // Debug: Verify containerID in response298 // Debug: Verify containerID in response
@@ -310,28 +381,33 @@ func saveAndImportDeployImage(c echo.Context, a *server.App, payload *common.Dep
310 }381 }
311 382
312 // createAndStartContainer creates and starts a new container383 // createAndStartContainer creates and starts a new container
313-func createAndStartContainer(a *server.App, payload *common.DeployPayload) (string, error) {384+func createAndStartContainer(a *server.App, payload *common.DeployPayload) (string, string, error) {
314 params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, payload.ImageID)385 params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, payload.ImageID)
315 if err != nil {386 if err != nil {
316- return "", fmt.Errorf("failed to create command parameters: %v", err)387+ return "", "", fmt.Errorf("failed to create command parameters: %v", err)
317 }388 }
318 389
319 containerID, err := docker.CreateContainer(params)390 containerID, err := docker.CreateContainer(params)
320 if err != nil {391 if err != nil {
321- return "", fmt.Errorf("failed to create container: %v", err)392+ return "", "", fmt.Errorf("failed to create container: %v", err)
393+ }
394+
395+ containerName, err := docker.GetContainerName(containerID)
396+ if err != nil {
397+ return "", "", fmt.Errorf("failed to get container name: %v", err)
322 }398 }
323 399
324 err = docker.StartContainer(containerID)400 err = docker.StartContainer(containerID)
325 if err != nil {401 if err != nil {
326 docker.RemoveContainer(containerID)402 docker.RemoveContainer(containerID)
327- return "", fmt.Errorf("failed to start container: %v", err)403+ return "", "", fmt.Errorf("failed to start container: %v", err)
328 }404 }
329 405
330- return containerID, nil406+ return containerID, containerName, nil
331 }407 }
332 408
333 // logResponse logs the deployment response409 // logResponse logs the deployment response
334 func logResponse(response DeployResponse) {410 func logResponse(response DeployResponse) {
335 responseJSON, _ := json.Marshal(response)411 responseJSON, _ := json.Marshal(response)
336- fmt.Printf("Server response: %s\n", string(responseJSON))412+ log.Info("Deployment response", string(responseJSON))
337 }413 }
M internal/httpserve/router.go +6 -0
@@ -70,6 +70,12 @@ func bindAPIEndpoints(e *echo.Echo, a *server.App) {
7070 protectedApiGroup.POST("/push", func(c echo.Context) error {
7171 return handlers.PostPush(c, a)
7272 })
73+ protectedApiGroup.POST("/stop", func(c echo.Context) error {
74+ return handlers.PostContainerStop(c, a)
75+ })
76+ protectedApiGroup.POST("/remove", func(c echo.Context) error {
77+ return handlers.PostContainerRemove(c, a)
78+ })
7379 }
7480
7581 // bindStaticRoute bind static path
@@ -70,6 +70,12 @@ func bindAPIEndpoints(e *echo.Echo, a *server.App) {
70 protectedApiGroup.POST("/push", func(c echo.Context) error {70 protectedApiGroup.POST("/push", func(c echo.Context) error {
71 return handlers.PostPush(c, a)71 return handlers.PostPush(c, a)
72 })72 })
73+ protectedApiGroup.POST("/stop", func(c echo.Context) error {
74+ return handlers.PostContainerStop(c, a)
75+ })
76+ protectedApiGroup.POST("/remove", func(c echo.Context) error {
77+ return handlers.PostContainerRemove(c, a)
78+ })
73 }79 }
74 80
75 // bindStaticRoute bind static path81 // bindStaticRoute bind static path

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