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) { | |||
| 115 | 115 | } | |
| 116 | 116 | ||
| 117 | 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) | |
| 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 | + | } | |
| 146 | 161 | } | |
| 147 | - | // Return nil to prevent further error logging | |
| 148 | 162 | return nil | |
| 149 | 163 | } | |
| 150 | - | log.Error("Error sending HTTP request", "error", err) | |
| 151 | - | return nil | |
| 152 | - | } | |
| 153 | 164 | ||
| 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 | + | } | |
| 159 | 170 | ||
| 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 | + | } | |
| 167 | 178 | ||
| 168 | - | return waitForDeployment(deployResponse.Domain, deployResponse.ContainerID) | |
| 179 | + | return waitForDeployment(deployResponse.Domain, deployResponse.ContainerID) | |
| 180 | + | } | |
| 169 | 181 | } | |
| 170 | 182 | ||
| 171 | 183 | 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.DeploymentError | 149 | + | return nil |
| 134 | - | if errors.As(err, &deployErr) { | 150 | + | } |
| 135 | - | // Try to parse the raw response to get the container ID | 151 | + | log.Info("Retrying deployment...") |
| 136 | - | var deployResponse common.DeployResponse | 152 | + | 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 ID | 160 | + | } |
| 145 | - | log.Error("Deployment failed", "error", deployErr.Message, "status_code", deployErr.StatusCode) | |||
| 146 | } | 161 | } | ||
| 147 | - | // Return nil to prevent further error logging | |||
| 148 | return nil | 162 | return nil | ||
| 149 | } | 163 | } | ||
| 150 | - | log.Error("Error sending HTTP request", "error", err) | |||
| 151 | - | return nil | |||
| 152 | - | } | |||
| 153 | 164 | ||||
| 154 | - | var deployResponse common.DeployResponse | 165 | + | 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 nil | 168 | + | 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 nil | 176 | + | 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 { | |||
| 19 | 19 | ||
| 20 | 20 | // DeployResponse represents the response structure for deployment requests | |
| 21 | 21 | 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"` | |
| 26 | 27 | } | |
| 27 | 28 | ||
| 28 | 29 | // PushResponse represents the response structure for push requests | |
| @@ -32,6 +33,21 @@ type PushResponse struct { | |||
| 32 | 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 | 51 | // DeploymentError represents an error that occurred during deployment | |
| 36 | 52 | type DeploymentError struct { | |
| 37 | 53 | StatusCode int `json:"status"` | |
| @@ -148,7 +164,8 @@ func NewPingPayload(data map[string]interface{}) (PingPayload, error) { | |||
| 148 | 164 | ||
| 149 | 165 | // StopPayload represents the payload for stop requests | |
| 150 | 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 | 171 | // GetType returns the type of the StopPayload | |
| @@ -158,7 +175,8 @@ func (p StopPayload) GetType() string { | |||
| 158 | 175 | ||
| 159 | 176 | // StartPayload represents the payload for start requests | |
| 160 | 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 | 182 | // GetType returns the type of the StartPayload | |
| @@ -19,10 +19,11 @@ type Payload interface { | |||||
| 19 | 19 | ||||
| 20 | // DeployResponse represents the response structure for deployment requests | 20 | // 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 requests | 29 | // 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 deployment | 51 | // 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 requests | 165 | // 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 StopPayload | 171 | // 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 requests | 176 | // 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 StartPayload | 182 | // GetType returns the type of the StartPayload | ||
M internal/httpserve/handlers/cli_endpoints.go +87 -11
| @@ -24,6 +24,8 @@ type InfoResponse struct { | |||
| 24 | 24 | ||
| 25 | 25 | type DeployResponse = common.DeployResponse | |
| 26 | 26 | type PushResponse = common.PushResponse | |
| 27 | + | type StopResponse = common.StopResponse | |
| 28 | + | type RemoveResponse = common.RemoveResponse | |
| 27 | 29 | ||
| 28 | 30 | // sendJSONResponse is a helper function to send JSON responses | |
| 29 | 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 | 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 | 137 | // PostPush handles the image push request | |
| 68 | 138 | func PostPush(c echo.Context, a *server.App) error { | |
| 69 | 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 | 264 | if err != nil { | |
| 195 | 265 | if strings.Contains(err.Error(), "is already in use") { | |
| 196 | 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 | 290 | 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, | |
| 225 | 296 | } | |
| 226 | 297 | ||
| 227 | 298 | // 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 | 383 | // 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 | 385 | params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, payload.ImageID) | |
| 315 | 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 | 390 | containerID, err := docker.CreateContainer(params) | |
| 320 | 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 | 400 | err = docker.StartContainer(containerID) | |
| 325 | 401 | if err != nil { | |
| 326 | 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, nil | |
| 406 | + | return containerID, containerName, nil | |
| 331 | 407 | } | |
| 332 | 408 | ||
| 333 | 409 | // logResponse logs the deployment response | |
| 334 | 410 | func logResponse(response DeployResponse) { | |
| 335 | 411 | responseJSON, _ := json.Marshal(response) | |
| 336 | - | fmt.Printf("Server response: %s\n", string(responseJSON)) | |
| 412 | + | log.Info("Deployment response", string(responseJSON)) | |
| 337 | 413 | } | |
| @@ -24,6 +24,8 @@ type InfoResponse struct { | |||||
| 24 | 24 | ||||
| 25 | type DeployResponse = common.DeployResponse | 25 | type DeployResponse = common.DeployResponse | ||
| 26 | type PushResponse = common.PushResponse | 26 | 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 responses | 30 | // 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 request | 137 | // 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 response | 298 | // 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 container | 383 | // 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, nil | 406 | + | return containerID, containerName, nil |
| 331 | } | 407 | } | ||
| 332 | 408 | ||||
| 333 | // logResponse logs the deployment response | 409 | // 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) { | |||
| 70 | 70 | protectedApiGroup.POST("/push", func(c echo.Context) error { | |
| 71 | 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 | 81 | // 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 path | 81 | // bindStaticRoute bind static path | ||