fix: resolve push command corrupted image import and unify command structure
- commit
- 8b732333378e81fbca8171a61c35317ef91579c8
- author
- Brice · · 2024-11-20 08:15 UTC
- parent
- cccb252
- signature
- unsigned
5 files · +284 -153
M internal/cli/cmd/deploy.go +2 -2
| @@ -44,7 +44,7 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||
| 44 | 44 | imageName := args[0] | |
| 45 | 45 | log.Info("Pushing image", "image", imageName) | |
| 46 | 46 | ||
| 47 | - | if err := validateInputs(imageName, port, targetDomain); err != nil { | |
| 47 | + | if err := validateDeployInputs(imageName, port, targetDomain); err != nil { | |
| 48 | 48 | log.Error("Validation failed", "error", err) | |
| 49 | 49 | return | |
| 50 | 50 | } | |
| @@ -208,7 +208,7 @@ func waitForDeployment(domain string, containerID string) error { | |||
| 208 | 208 | return fmt.Errorf("deployment not ready after %d attempts", maxRetries) | |
| 209 | 209 | } | |
| 210 | 210 | ||
| 211 | - | func validateInputs(imageName, port, targetDomain string) error { | |
| 211 | + | func validateDeployInputs(imageName, port, targetDomain string) error { | |
| 212 | 212 | if err := handler.ValidateImageName(imageName); err != nil { | |
| 213 | 213 | return err | |
| 214 | 214 | } | |
| @@ -44,7 +44,7 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||||
| 44 | imageName := args[0] | 44 | imageName := args[0] | ||
| 45 | log.Info("Pushing image", "image", imageName) | 45 | log.Info("Pushing image", "image", imageName) | ||
| 46 | 46 | ||||
| 47 | - | if err := validateInputs(imageName, port, targetDomain); err != nil { | 47 | + | if err := validateDeployInputs(imageName, port, targetDomain); err != nil { |
| 48 | log.Error("Validation failed", "error", err) | 48 | log.Error("Validation failed", "error", err) | ||
| 49 | return | 49 | return | ||
| 50 | } | 50 | } | ||
| @@ -208,7 +208,7 @@ func waitForDeployment(domain string, containerID string) error { | |||||
| 208 | return fmt.Errorf("deployment not ready after %d attempts", maxRetries) | 208 | return fmt.Errorf("deployment not ready after %d attempts", maxRetries) | ||
| 209 | } | 209 | } | ||
| 210 | 210 | ||||
| 211 | - | func validateInputs(imageName, port, targetDomain string) error { | 211 | + | func validateDeployInputs(imageName, port, targetDomain string) error { |
| 212 | if err := handler.ValidateImageName(imageName); err != nil { | 212 | if err := handler.ValidateImageName(imageName); err != nil { | ||
| 213 | return err | 213 | return err | ||
| 214 | } | 214 | } | ||
M internal/cli/cmd/push.go +44 -58
| @@ -1,17 +1,19 @@ | |||
| 1 | 1 | package cmd | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "bytes" | |
| 4 | 5 | "context" | |
| 5 | 6 | "encoding/json" | |
| 6 | 7 | "fmt" | |
| 8 | + | "io" | |
| 7 | 9 | "net/http" | |
| 8 | 10 | "os" | |
| 9 | 11 | "strings" | |
| 12 | + | "time" | |
| 10 | 13 | ||
| 11 | 14 | "github.com/bnema/gordon/internal/cli" | |
| 12 | 15 | "github.com/bnema/gordon/internal/cli/handler" | |
| 13 | 16 | "github.com/bnema/gordon/internal/common" | |
| 14 | - | "github.com/bnema/gordon/pkg/docker" | |
| 15 | 17 | "github.com/charmbracelet/log" | |
| 16 | 18 | "github.com/spf13/cobra" | |
| 17 | 19 | ) | |
| @@ -38,8 +40,23 @@ func NewPushCommand(a *cli.App) *cobra.Command { | |||
| 38 | 40 | imageName := args[0] | |
| 39 | 41 | log.Info("Pushing image", "image", imageName) | |
| 40 | 42 | ||
| 41 | - | if err := pushImage(a, imageName); err != nil { | |
| 42 | - | log.Error("Push failed", "error", err) | |
| 43 | + | if err := validatePushInputs(imageName); err != nil { | |
| 44 | + | log.Error("Validation failed", "error", err) | |
| 45 | + | return | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | reader, actualSize, err := handler.ExportDockerImage(imageName) | |
| 49 | + | if err != nil { | |
| 50 | + | log.Error("Error exporting image", "error", err) | |
| 51 | + | return | |
| 52 | + | } | |
| 53 | + | defer reader.Close() | |
| 54 | + | ||
| 55 | + | sizeInMB := float64(actualSize) / 1024 / 1024 | |
| 56 | + | ||
| 57 | + | log.Info("Image exported successfully", "image", imageName, "size", fmt.Sprintf("%.2fMB", sizeInMB)) | |
| 58 | + | ||
| 59 | + | if err := pushImage(a, reader, imageName); err != nil { | |
| 43 | 60 | os.Exit(1) | |
| 44 | 61 | } | |
| 45 | 62 | }, | |
| @@ -50,74 +67,36 @@ func NewPushCommand(a *cli.App) *cobra.Command { | |||
| 50 | 67 | return pushCmd | |
| 51 | 68 | } | |
| 52 | 69 | ||
| 53 | - | func pushImage(a *cli.App, imageName string) error { | |
| 70 | + | func pushImage(a *cli.App, reader io.Reader, imageName string) error { | |
| 54 | 71 | // Check authentication early | |
| 55 | 72 | if err := handler.CheckAndRefreshAuth(a); err != nil { | |
| 56 | 73 | log.Error("Authentication check failed", "error", err) | |
| 57 | 74 | return err | |
| 58 | 75 | } | |
| 59 | 76 | ||
| 60 | - | if err := handler.ValidateImageName(imageName); err != nil { | |
| 61 | - | return fmt.Errorf("invalid image name: %w", err) | |
| 62 | - | } | |
| 63 | - | ||
| 64 | - | handler.EnsureImageTag(&imageName) | |
| 65 | - | ||
| 66 | - | reader, actualSize, err := handler.ExportDockerImage(imageName) | |
| 67 | - | if err != nil { | |
| 68 | - | return fmt.Errorf("error exporting image: %w", err) | |
| 69 | - | } | |
| 70 | - | defer reader.Close() | |
| 71 | - | ||
| 72 | - | _, err = docker.GetImageIDByName(imageName) | |
| 77 | + | var buf bytes.Buffer | |
| 78 | + | size, err := io.Copy(&buf, reader) | |
| 73 | 79 | if err != nil { | |
| 74 | - | return fmt.Errorf("failed to get image ID: %w", err) | |
| 80 | + | return fmt.Errorf("failed to read image data: %w", err) | |
| 75 | 81 | } | |
| 76 | 82 | ||
| 77 | - | sizeInMB := float64(actualSize) / 1024 / 1024 | |
| 78 | - | log.Info("Image exported successfully", | |
| 83 | + | imageReader := bytes.NewReader(buf.Bytes()) | |
| 84 | + | sizeInMB := float64(size) / 1024 / 1024 | |
| 85 | + | log.Info("Attempting to push...", | |
| 79 | 86 | "image", imageName, | |
| 80 | - | "size", fmt.Sprintf("%.2fMB", sizeInMB)) | |
| 81 | - | ||
| 82 | - | var resp *handler.Response | |
| 83 | - | ||
| 84 | - | // Use chunked endpoint if image is larger than 10MB | |
| 85 | - | if sizeInMB > 10 { | |
| 86 | - | chunkedClient := handler.NewChunkedClient(a) | |
| 87 | - | if chunkedClient == nil { | |
| 88 | - | return fmt.Errorf("failed to initialize chunked client - authentication issue") | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | ctx := context.Background() | |
| 92 | - | headers := make(http.Header) | |
| 93 | - | // Add any necessary headers here | |
| 94 | - | headers.Set("Content-Type", "application/octet-stream") | |
| 95 | - | ||
| 96 | - | var err error | |
| 97 | - | resp, err = chunkedClient.SendFileAsChunks(ctx, "/push/chunked", headers, reader, actualSize, imageName) | |
| 98 | - | if err != nil { | |
| 99 | - | return fmt.Errorf("chunked transfer failed: %w", err) | |
| 100 | - | } | |
| 101 | - | } else { | |
| 102 | - | // Use regular push endpoint for smaller images | |
| 103 | - | reqPayload := common.RequestPayload{ | |
| 104 | - | Type: "push", | |
| 105 | - | Payload: common.PushPayload{ | |
| 106 | - | ImageName: imageName, | |
| 107 | - | Data: reader, | |
| 108 | - | }, | |
| 109 | - | } | |
| 110 | - | var err error | |
| 111 | - | resp, err = handler.SendHTTPRequest(a, &reqPayload, "POST", "/push") | |
| 112 | - | if err != nil { | |
| 113 | - | return fmt.Errorf("regular push failed: %w", err) | |
| 114 | - | } | |
| 115 | - | } | |
| 87 | + | "size", fmt.Sprintf("%.2fMB", sizeInMB), | |
| 88 | + | ) | |
| 116 | 89 | ||
| 117 | - | if resp == nil { | |
| 118 | - | return fmt.Errorf("received nil response from server") | |
| 90 | + | headers := http.Header{ | |
| 91 | + | "X-Image-Name": {imageName}, | |
| 92 | + | "Content-Type": {"application/octet-stream"}, | |
| 119 | 93 | } | |
| 120 | 94 | ||
| 95 | + | chunkedClient := handler.NewChunkedClient(a) | |
| 96 | + | ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute) | |
| 97 | + | defer cancel() | |
| 98 | + | ||
| 99 | + | resp, err := chunkedClient.SendFileAsChunks(ctx, "/push/chunked", headers, imageReader, size, imageName) | |
| 121 | 100 | var pushResponse common.PushResponse | |
| 122 | 101 | if err := json.Unmarshal(resp.Body, &pushResponse); err != nil { | |
| 123 | 102 | log.Error("Error parsing response", "error", err) | |
| @@ -139,3 +118,10 @@ func pushImage(a *cli.App, imageName string) error { | |||
| 139 | 118 | ||
| 140 | 119 | return nil | |
| 141 | 120 | } | |
| 121 | + | ||
| 122 | + | func validatePushInputs(imageName string) error { | |
| 123 | + | if err := handler.ValidateImageName(imageName); err != nil { | |
| 124 | + | return err | |
| 125 | + | } | |
| 126 | + | return handler.EnsureImageTag(&imageName) | |
| 127 | + | } | |
| @@ -1,17 +1,19 @@ | |||||
| 1 | package cmd | 1 | package cmd | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "bytes" | |||
| 4 | "context" | 5 | "context" | ||
| 5 | "encoding/json" | 6 | "encoding/json" | ||
| 6 | "fmt" | 7 | "fmt" | ||
| 8 | + | "io" | |||
| 7 | "net/http" | 9 | "net/http" | ||
| 8 | "os" | 10 | "os" | ||
| 9 | "strings" | 11 | "strings" | ||
| 12 | + | "time" | |||
| 10 | 13 | ||||
| 11 | "github.com/bnema/gordon/internal/cli" | 14 | "github.com/bnema/gordon/internal/cli" | ||
| 12 | "github.com/bnema/gordon/internal/cli/handler" | 15 | "github.com/bnema/gordon/internal/cli/handler" | ||
| 13 | "github.com/bnema/gordon/internal/common" | 16 | "github.com/bnema/gordon/internal/common" | ||
| 14 | - | "github.com/bnema/gordon/pkg/docker" | |||
| 15 | "github.com/charmbracelet/log" | 17 | "github.com/charmbracelet/log" | ||
| 16 | "github.com/spf13/cobra" | 18 | "github.com/spf13/cobra" | ||
| 17 | ) | 19 | ) | ||
| @@ -38,8 +40,23 @@ func NewPushCommand(a *cli.App) *cobra.Command { | |||||
| 38 | imageName := args[0] | 40 | imageName := args[0] | ||
| 39 | log.Info("Pushing image", "image", imageName) | 41 | log.Info("Pushing image", "image", imageName) | ||
| 40 | 42 | ||||
| 41 | - | if err := pushImage(a, imageName); err != nil { | 43 | + | if err := validatePushInputs(imageName); err != nil { |
| 42 | - | log.Error("Push failed", "error", err) | 44 | + | log.Error("Validation failed", "error", err) |
| 45 | + | return | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | reader, actualSize, err := handler.ExportDockerImage(imageName) | |||
| 49 | + | if err != nil { | |||
| 50 | + | log.Error("Error exporting image", "error", err) | |||
| 51 | + | return | |||
| 52 | + | } | |||
| 53 | + | defer reader.Close() | |||
| 54 | + | ||||
| 55 | + | sizeInMB := float64(actualSize) / 1024 / 1024 | |||
| 56 | + | ||||
| 57 | + | log.Info("Image exported successfully", "image", imageName, "size", fmt.Sprintf("%.2fMB", sizeInMB)) | |||
| 58 | + | ||||
| 59 | + | if err := pushImage(a, reader, imageName); err != nil { | |||
| 43 | os.Exit(1) | 60 | os.Exit(1) | ||
| 44 | } | 61 | } | ||
| 45 | }, | 62 | }, | ||
| @@ -50,74 +67,36 @@ func NewPushCommand(a *cli.App) *cobra.Command { | |||||
| 50 | return pushCmd | 67 | return pushCmd | ||
| 51 | } | 68 | } | ||
| 52 | 69 | ||||
| 53 | - | func pushImage(a *cli.App, imageName string) error { | 70 | + | func pushImage(a *cli.App, reader io.Reader, imageName string) error { |
| 54 | // Check authentication early | 71 | // Check authentication early | ||
| 55 | if err := handler.CheckAndRefreshAuth(a); err != nil { | 72 | if err := handler.CheckAndRefreshAuth(a); err != nil { | ||
| 56 | log.Error("Authentication check failed", "error", err) | 73 | log.Error("Authentication check failed", "error", err) | ||
| 57 | return err | 74 | return err | ||
| 58 | } | 75 | } | ||
| 59 | 76 | ||||
| 60 | - | if err := handler.ValidateImageName(imageName); err != nil { | 77 | + | var buf bytes.Buffer |
| 61 | - | return fmt.Errorf("invalid image name: %w", err) | 78 | + | size, err := io.Copy(&buf, reader) |
| 62 | - | } | |||
| 63 | - | ||||
| 64 | - | handler.EnsureImageTag(&imageName) | |||
| 65 | - | ||||
| 66 | - | reader, actualSize, err := handler.ExportDockerImage(imageName) | |||
| 67 | - | if err != nil { | |||
| 68 | - | return fmt.Errorf("error exporting image: %w", err) | |||
| 69 | - | } | |||
| 70 | - | defer reader.Close() | |||
| 71 | - | ||||
| 72 | - | _, err = docker.GetImageIDByName(imageName) | |||
| 73 | if err != nil { | 79 | if err != nil { | ||
| 74 | - | return fmt.Errorf("failed to get image ID: %w", err) | 80 | + | return fmt.Errorf("failed to read image data: %w", err) |
| 75 | } | 81 | } | ||
| 76 | 82 | ||||
| 77 | - | sizeInMB := float64(actualSize) / 1024 / 1024 | 83 | + | imageReader := bytes.NewReader(buf.Bytes()) |
| 78 | - | log.Info("Image exported successfully", | 84 | + | sizeInMB := float64(size) / 1024 / 1024 |
| 85 | + | log.Info("Attempting to push...", | |||
| 79 | "image", imageName, | 86 | "image", imageName, | ||
| 80 | - | "size", fmt.Sprintf("%.2fMB", sizeInMB)) | 87 | + | "size", fmt.Sprintf("%.2fMB", sizeInMB), |
| 81 | - | 88 | + | ) | |
| 82 | - | var resp *handler.Response | |||
| 83 | - | ||||
| 84 | - | // Use chunked endpoint if image is larger than 10MB | |||
| 85 | - | if sizeInMB > 10 { | |||
| 86 | - | chunkedClient := handler.NewChunkedClient(a) | |||
| 87 | - | if chunkedClient == nil { | |||
| 88 | - | return fmt.Errorf("failed to initialize chunked client - authentication issue") | |||
| 89 | - | } | |||
| 90 | - | ||||
| 91 | - | ctx := context.Background() | |||
| 92 | - | headers := make(http.Header) | |||
| 93 | - | // Add any necessary headers here | |||
| 94 | - | headers.Set("Content-Type", "application/octet-stream") | |||
| 95 | - | ||||
| 96 | - | var err error | |||
| 97 | - | resp, err = chunkedClient.SendFileAsChunks(ctx, "/push/chunked", headers, reader, actualSize, imageName) | |||
| 98 | - | if err != nil { | |||
| 99 | - | return fmt.Errorf("chunked transfer failed: %w", err) | |||
| 100 | - | } | |||
| 101 | - | } else { | |||
| 102 | - | // Use regular push endpoint for smaller images | |||
| 103 | - | reqPayload := common.RequestPayload{ | |||
| 104 | - | Type: "push", | |||
| 105 | - | Payload: common.PushPayload{ | |||
| 106 | - | ImageName: imageName, | |||
| 107 | - | Data: reader, | |||
| 108 | - | }, | |||
| 109 | - | } | |||
| 110 | - | var err error | |||
| 111 | - | resp, err = handler.SendHTTPRequest(a, &reqPayload, "POST", "/push") | |||
| 112 | - | if err != nil { | |||
| 113 | - | return fmt.Errorf("regular push failed: %w", err) | |||
| 114 | - | } | |||
| 115 | - | } | |||
| 116 | 89 | ||||
| 117 | - | if resp == nil { | 90 | + | headers := http.Header{ |
| 118 | - | return fmt.Errorf("received nil response from server") | 91 | + | "X-Image-Name": {imageName}, |
| 92 | + | "Content-Type": {"application/octet-stream"}, | |||
| 119 | } | 93 | } | ||
| 120 | 94 | ||||
| 95 | + | chunkedClient := handler.NewChunkedClient(a) | |||
| 96 | + | ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute) | |||
| 97 | + | defer cancel() | |||
| 98 | + | ||||
| 99 | + | resp, err := chunkedClient.SendFileAsChunks(ctx, "/push/chunked", headers, imageReader, size, imageName) | |||
| 121 | var pushResponse common.PushResponse | 100 | var pushResponse common.PushResponse | ||
| 122 | if err := json.Unmarshal(resp.Body, &pushResponse); err != nil { | 101 | if err := json.Unmarshal(resp.Body, &pushResponse); err != nil { | ||
| 123 | log.Error("Error parsing response", "error", err) | 102 | log.Error("Error parsing response", "error", err) | ||
| @@ -139,3 +118,10 @@ func pushImage(a *cli.App, imageName string) error { | |||||
| 139 | 118 | ||||
| 140 | return nil | 119 | return nil | ||
| 141 | } | 120 | } | ||
| 121 | + | ||||
| 122 | + | func validatePushInputs(imageName string) error { | |||
| 123 | + | if err := handler.ValidateImageName(imageName); err != nil { | |||
| 124 | + | return err | |||
| 125 | + | } | |||
| 126 | + | return handler.EnsureImageTag(&imageName) | |||
| 127 | + | } | |||
M internal/httpserve/handlers/endpoint_deploy.go +37 -8
| @@ -164,6 +164,27 @@ func handleChunkedDeploy(c echo.Context, a *server.App, metadataStr string) erro | |||
| 164 | 164 | }) | |
| 165 | 165 | } | |
| 166 | 166 | ||
| 167 | + | // Add chunk validation | |
| 168 | + | if len(chunkData) == 0 { | |
| 169 | + | log.Error("Received empty chunk") | |
| 170 | + | return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{ | |
| 171 | + | Success: false, | |
| 172 | + | Message: "Received empty chunk", | |
| 173 | + | }) | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | // Verify chunk size matches metadata | |
| 177 | + | if int64(len(chunkData)) != metadata.ChunkSize { | |
| 178 | + | log.Error("Chunk size mismatch", | |
| 179 | + | "expected", metadata.ChunkSize, | |
| 180 | + | "received", len(chunkData)) | |
| 181 | + | return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{ | |
| 182 | + | Success: false, | |
| 183 | + | Message: fmt.Sprintf("Chunk size mismatch: expected %d, got %d", | |
| 184 | + | metadata.ChunkSize, len(chunkData)), | |
| 185 | + | }) | |
| 186 | + | } | |
| 187 | + | ||
| 167 | 188 | // Store the complete chunk | |
| 168 | 189 | chunkStore.chunks[metadata.TransferID][metadata.ChunkNumber] = chunkData | |
| 169 | 190 | chunkStore.mu.Unlock() | |
| @@ -188,25 +209,25 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe | |||
| 188 | 209 | // Get transfer data | |
| 189 | 210 | metadata, chunks, err := getTransferData(transferID) | |
| 190 | 211 | if err != nil { | |
| 191 | - | return sendErrorResponse(c, "Failed to get transfer data", err) | |
| 212 | + | return sendDeployErrorResponse(c, "Failed to get transfer data", err) | |
| 192 | 213 | } | |
| 193 | 214 | ||
| 194 | 215 | // Create temporary directory for processing | |
| 195 | 216 | tmpDir, err := os.MkdirTemp("", "docker-deploy-*") | |
| 196 | 217 | if err != nil { | |
| 197 | - | return sendErrorResponse(c, "Failed to create temp directory", err) | |
| 218 | + | return sendDeployErrorResponse(c, "Failed to create temp directory", err) | |
| 198 | 219 | } | |
| 199 | 220 | defer os.RemoveAll(tmpDir) | |
| 200 | 221 | ||
| 201 | 222 | tmpFile, err := os.Create(filepath.Join(tmpDir, "image.tar")) | |
| 202 | 223 | if err != nil { | |
| 203 | - | return sendErrorResponse(c, "Failed to create temp file", err) | |
| 224 | + | return sendDeployErrorResponse(c, "Failed to create temp file", err) | |
| 204 | 225 | } | |
| 205 | 226 | defer tmpFile.Close() | |
| 206 | 227 | ||
| 207 | 228 | // Write chunks to file | |
| 208 | 229 | if err := writeChunksToFile(tmpFile, chunks, metadata.TotalChunks); err != nil { | |
| 209 | - | return sendErrorResponse(c, "Failed to write chunks", err) | |
| 230 | + | return sendDeployErrorResponse(c, "Failed to write chunks", err) | |
| 210 | 231 | } | |
| 211 | 232 | ||
| 212 | 233 | // Get deployment-specific headers | |
| @@ -215,7 +236,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe | |||
| 215 | 236 | imageName := c.Request().Header.Get("X-Image-Name") | |
| 216 | 237 | ||
| 217 | 238 | if port == "" || targetDomain == "" || imageName == "" { | |
| 218 | - | return sendErrorResponse(c, "Missing required deployment parameters", | |
| 239 | + | return sendDeployErrorResponse(c, "Missing required deployment parameters", | |
| 219 | 240 | fmt.Errorf("port: %s, domain: %s, image: %s", port, targetDomain, imageName)) | |
| 220 | 241 | } | |
| 221 | 242 | ||
| @@ -224,7 +245,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe | |||
| 224 | 245 | containerName := strings.Split(cleanDomain, ".")[0] | |
| 225 | 246 | ||
| 226 | 247 | if containerName == "" { | |
| 227 | - | return sendErrorResponse(c, "Invalid target domain", | |
| 248 | + | return sendDeployErrorResponse(c, "Invalid target domain", | |
| 228 | 249 | fmt.Errorf("couldn't extract container name from domain: %s", targetDomain)) | |
| 229 | 250 | } | |
| 230 | 251 | ||
| @@ -259,7 +280,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe | |||
| 259 | 280 | RawResponse: err.Error(), | |
| 260 | 281 | }) | |
| 261 | 282 | } | |
| 262 | - | return sendErrorResponse(c, "Failed to import image", err) | |
| 283 | + | return sendDeployErrorResponse(c, "Failed to import image", err) | |
| 263 | 284 | } | |
| 264 | 285 | ||
| 265 | 286 | // Prepare deployment payload | |
| @@ -285,7 +306,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe | |||
| 285 | 306 | }) | |
| 286 | 307 | } | |
| 287 | 308 | ||
| 288 | - | return sendErrorResponse(c, "Failed to create or start container", err) | |
| 309 | + | return sendDeployErrorResponse(c, "Failed to create or start container", err) | |
| 289 | 310 | } | |
| 290 | 311 | ||
| 291 | 312 | // Cleanup transfer data | |
| @@ -305,6 +326,14 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe | |||
| 305 | 326 | }) | |
| 306 | 327 | } | |
| 307 | 328 | ||
| 329 | + | func sendDeployErrorResponse(c echo.Context, message string, err error) error { | |
| 330 | + | log.Error(message, "error", err) | |
| 331 | + | return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{ | |
| 332 | + | Success: false, | |
| 333 | + | Message: message, | |
| 334 | + | }) | |
| 335 | + | } | |
| 336 | + | ||
| 308 | 337 | // func waitForDeployment(domain string, containerID string) error { | |
| 309 | 338 | // client := &http.Client{Timeout: 10 * time.Second} | |
| 310 | 339 | // maxRetries := 10 | |
| @@ -164,6 +164,27 @@ func handleChunkedDeploy(c echo.Context, a *server.App, metadataStr string) erro | |||||
| 164 | }) | 164 | }) | ||
| 165 | } | 165 | } | ||
| 166 | 166 | ||||
| 167 | + | // Add chunk validation | |||
| 168 | + | if len(chunkData) == 0 { | |||
| 169 | + | log.Error("Received empty chunk") | |||
| 170 | + | return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{ | |||
| 171 | + | Success: false, | |||
| 172 | + | Message: "Received empty chunk", | |||
| 173 | + | }) | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | // Verify chunk size matches metadata | |||
| 177 | + | if int64(len(chunkData)) != metadata.ChunkSize { | |||
| 178 | + | log.Error("Chunk size mismatch", | |||
| 179 | + | "expected", metadata.ChunkSize, | |||
| 180 | + | "received", len(chunkData)) | |||
| 181 | + | return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{ | |||
| 182 | + | Success: false, | |||
| 183 | + | Message: fmt.Sprintf("Chunk size mismatch: expected %d, got %d", | |||
| 184 | + | metadata.ChunkSize, len(chunkData)), | |||
| 185 | + | }) | |||
| 186 | + | } | |||
| 187 | + | ||||
| 167 | // Store the complete chunk | 188 | // Store the complete chunk | ||
| 168 | chunkStore.chunks[metadata.TransferID][metadata.ChunkNumber] = chunkData | 189 | chunkStore.chunks[metadata.TransferID][metadata.ChunkNumber] = chunkData | ||
| 169 | chunkStore.mu.Unlock() | 190 | chunkStore.mu.Unlock() | ||
| @@ -188,25 +209,25 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe | |||||
| 188 | // Get transfer data | 209 | // Get transfer data | ||
| 189 | metadata, chunks, err := getTransferData(transferID) | 210 | metadata, chunks, err := getTransferData(transferID) | ||
| 190 | if err != nil { | 211 | if err != nil { | ||
| 191 | - | return sendErrorResponse(c, "Failed to get transfer data", err) | 212 | + | return sendDeployErrorResponse(c, "Failed to get transfer data", err) |
| 192 | } | 213 | } | ||
| 193 | 214 | ||||
| 194 | // Create temporary directory for processing | 215 | // Create temporary directory for processing | ||
| 195 | tmpDir, err := os.MkdirTemp("", "docker-deploy-*") | 216 | tmpDir, err := os.MkdirTemp("", "docker-deploy-*") | ||
| 196 | if err != nil { | 217 | if err != nil { | ||
| 197 | - | return sendErrorResponse(c, "Failed to create temp directory", err) | 218 | + | return sendDeployErrorResponse(c, "Failed to create temp directory", err) |
| 198 | } | 219 | } | ||
| 199 | defer os.RemoveAll(tmpDir) | 220 | defer os.RemoveAll(tmpDir) | ||
| 200 | 221 | ||||
| 201 | tmpFile, err := os.Create(filepath.Join(tmpDir, "image.tar")) | 222 | tmpFile, err := os.Create(filepath.Join(tmpDir, "image.tar")) | ||
| 202 | if err != nil { | 223 | if err != nil { | ||
| 203 | - | return sendErrorResponse(c, "Failed to create temp file", err) | 224 | + | return sendDeployErrorResponse(c, "Failed to create temp file", err) |
| 204 | } | 225 | } | ||
| 205 | defer tmpFile.Close() | 226 | defer tmpFile.Close() | ||
| 206 | 227 | ||||
| 207 | // Write chunks to file | 228 | // Write chunks to file | ||
| 208 | if err := writeChunksToFile(tmpFile, chunks, metadata.TotalChunks); err != nil { | 229 | if err := writeChunksToFile(tmpFile, chunks, metadata.TotalChunks); err != nil { | ||
| 209 | - | return sendErrorResponse(c, "Failed to write chunks", err) | 230 | + | return sendDeployErrorResponse(c, "Failed to write chunks", err) |
| 210 | } | 231 | } | ||
| 211 | 232 | ||||
| 212 | // Get deployment-specific headers | 233 | // Get deployment-specific headers | ||
| @@ -215,7 +236,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe | |||||
| 215 | imageName := c.Request().Header.Get("X-Image-Name") | 236 | imageName := c.Request().Header.Get("X-Image-Name") | ||
| 216 | 237 | ||||
| 217 | if port == "" || targetDomain == "" || imageName == "" { | 238 | if port == "" || targetDomain == "" || imageName == "" { | ||
| 218 | - | return sendErrorResponse(c, "Missing required deployment parameters", | 239 | + | return sendDeployErrorResponse(c, "Missing required deployment parameters", |
| 219 | fmt.Errorf("port: %s, domain: %s, image: %s", port, targetDomain, imageName)) | 240 | fmt.Errorf("port: %s, domain: %s, image: %s", port, targetDomain, imageName)) | ||
| 220 | } | 241 | } | ||
| 221 | 242 | ||||
| @@ -224,7 +245,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe | |||||
| 224 | containerName := strings.Split(cleanDomain, ".")[0] | 245 | containerName := strings.Split(cleanDomain, ".")[0] | ||
| 225 | 246 | ||||
| 226 | if containerName == "" { | 247 | if containerName == "" { | ||
| 227 | - | return sendErrorResponse(c, "Invalid target domain", | 248 | + | return sendDeployErrorResponse(c, "Invalid target domain", |
| 228 | fmt.Errorf("couldn't extract container name from domain: %s", targetDomain)) | 249 | fmt.Errorf("couldn't extract container name from domain: %s", targetDomain)) | ||
| 229 | } | 250 | } | ||
| 230 | 251 | ||||
| @@ -259,7 +280,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe | |||||
| 259 | RawResponse: err.Error(), | 280 | RawResponse: err.Error(), | ||
| 260 | }) | 281 | }) | ||
| 261 | } | 282 | } | ||
| 262 | - | return sendErrorResponse(c, "Failed to import image", err) | 283 | + | return sendDeployErrorResponse(c, "Failed to import image", err) |
| 263 | } | 284 | } | ||
| 264 | 285 | ||||
| 265 | // Prepare deployment payload | 286 | // Prepare deployment payload | ||
| @@ -285,7 +306,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe | |||||
| 285 | }) | 306 | }) | ||
| 286 | } | 307 | } | ||
| 287 | 308 | ||||
| 288 | - | return sendErrorResponse(c, "Failed to create or start container", err) | 309 | + | return sendDeployErrorResponse(c, "Failed to create or start container", err) |
| 289 | } | 310 | } | ||
| 290 | 311 | ||||
| 291 | // Cleanup transfer data | 312 | // Cleanup transfer data | ||
| @@ -305,6 +326,14 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe | |||||
| 305 | }) | 326 | }) | ||
| 306 | } | 327 | } | ||
| 307 | 328 | ||||
| 329 | + | func sendDeployErrorResponse(c echo.Context, message string, err error) error { | |||
| 330 | + | log.Error(message, "error", err) | |||
| 331 | + | return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{ | |||
| 332 | + | Success: false, | |||
| 333 | + | Message: message, | |||
| 334 | + | }) | |||
| 335 | + | } | |||
| 336 | + | ||||
| 308 | // func waitForDeployment(domain string, containerID string) error { | 337 | // func waitForDeployment(domain string, containerID string) error { | ||
| 309 | // client := &http.Client{Timeout: 10 * time.Second} | 338 | // client := &http.Client{Timeout: 10 * time.Second} | ||
| 310 | // maxRetries := 10 | 339 | // maxRetries := 10 | ||
M internal/httpserve/handlers/endpoint_helpers.go +103 -13
| @@ -2,9 +2,12 @@ | |||
| 2 | 2 | package handlers | |
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | + | "bytes" | |
| 5 | 6 | "errors" | |
| 6 | 7 | "fmt" | |
| 8 | + | "io" | |
| 7 | 9 | "net/http" | |
| 10 | + | "os" | |
| 8 | 11 | "regexp" | |
| 9 | 12 | "sync" | |
| 10 | 13 | "time" | |
| @@ -100,19 +103,6 @@ func GetInfos(c echo.Context, a *server.App) error { | |||
| 100 | 103 | return sendJSONResponse(c, http.StatusOK, info) | |
| 101 | 104 | } | |
| 102 | 105 | ||
| 103 | - | // validateAndPreparePushPayload validates and prepares the push payload | |
| 104 | - | func validateAndPreparePushPayload(c echo.Context) (*common.PushPayload, error) { | |
| 105 | - | payload := &common.PushPayload{ | |
| 106 | - | ImageName: c.Request().Header.Get("X-Image-Name"), | |
| 107 | - | } | |
| 108 | - | ||
| 109 | - | if payload.ImageName == "" { | |
| 110 | - | return nil, errors.New("invalid image name") | |
| 111 | - | } | |
| 112 | - | ||
| 113 | - | return payload, nil | |
| 114 | - | } | |
| 115 | - | ||
| 116 | 106 | // validateAndPrepareDeployPayload validates and prepares the deploy payload | |
| 117 | 107 | func validateAndPrepareDeployPayload(c echo.Context) (*common.DeployPayload, error) { | |
| 118 | 108 | payload := &common.DeployPayload{ | |
| @@ -353,3 +343,103 @@ func extractContainerID(errorMessage string) string { | |||
| 353 | 343 | func storeIDMapping(shortID, fullID string) { | |
| 354 | 344 | safelyInteractWithIDMap(Update, shortID, fullID) | |
| 355 | 345 | } | |
| 346 | + | ||
| 347 | + | func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) error { | |
| 348 | + | // Validate input parameters | |
| 349 | + | if file == nil { | |
| 350 | + | return errors.New("nil file handle provided") | |
| 351 | + | } | |
| 352 | + | if len(chunks) == 0 { | |
| 353 | + | return errors.New("no chunks provided") | |
| 354 | + | } | |
| 355 | + | if totalChunks <= 0 { | |
| 356 | + | return fmt.Errorf("invalid total chunks count: %d", totalChunks) | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | // Verify we have all chunks before starting | |
| 360 | + | if len(chunks) != totalChunks { | |
| 361 | + | return fmt.Errorf("chunk count mismatch: got %d, expected %d", len(chunks), totalChunks) | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | // Calculate total expected size | |
| 365 | + | var totalSize int64 | |
| 366 | + | for _, chunk := range chunks { | |
| 367 | + | totalSize += int64(len(chunk)) | |
| 368 | + | } | |
| 369 | + | ||
| 370 | + | log.Debug("Starting chunk assembly", | |
| 371 | + | "totalChunks", totalChunks, | |
| 372 | + | "totalSize", fmt.Sprintf("%.2f MB", float64(totalSize)/(1024*1024))) | |
| 373 | + | ||
| 374 | + | // First, seek to the beginning of the file | |
| 375 | + | if _, err := file.Seek(0, 0); err != nil { | |
| 376 | + | return fmt.Errorf("failed to seek to beginning: %w", err) | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | // Create a temporary buffer with capacity hint | |
| 380 | + | tempBuffer := bytes.NewBuffer(make([]byte, 0, totalSize)) | |
| 381 | + | ||
| 382 | + | // Write chunks in order with validation | |
| 383 | + | for i := 0; i < totalChunks; i++ { | |
| 384 | + | chunk, exists := chunks[i] | |
| 385 | + | if !exists { | |
| 386 | + | return fmt.Errorf("missing chunk %d", i) | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | if len(chunk) == 0 { | |
| 390 | + | return fmt.Errorf("empty chunk found at index %d", i) | |
| 391 | + | } | |
| 392 | + | ||
| 393 | + | n, err := tempBuffer.Write(chunk) | |
| 394 | + | if err != nil { | |
| 395 | + | return fmt.Errorf("failed to write chunk %d to buffer: %w", i, err) | |
| 396 | + | } | |
| 397 | + | if n != len(chunk) { | |
| 398 | + | return fmt.Errorf("incomplete chunk write: wrote %d of %d bytes for chunk %d", | |
| 399 | + | n, len(chunk), i) | |
| 400 | + | } | |
| 401 | + | } | |
| 402 | + | ||
| 403 | + | // Verify buffer size matches expected total | |
| 404 | + | if int64(tempBuffer.Len()) != totalSize { | |
| 405 | + | return fmt.Errorf("buffer size mismatch: got %d, expected %d", | |
| 406 | + | tempBuffer.Len(), totalSize) | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | // Write the complete buffer to file | |
| 410 | + | written, err := io.Copy(file, tempBuffer) | |
| 411 | + | if err != nil { | |
| 412 | + | return fmt.Errorf("failed to write buffer to file: %w", err) | |
| 413 | + | } | |
| 414 | + | if written != totalSize { | |
| 415 | + | return fmt.Errorf("file write size mismatch: wrote %d, expected %d", | |
| 416 | + | written, totalSize) | |
| 417 | + | } | |
| 418 | + | ||
| 419 | + | // Ensure all data is written to disk | |
| 420 | + | if err := file.Sync(); err != nil { | |
| 421 | + | return fmt.Errorf("failed to sync file: %w", err) | |
| 422 | + | } | |
| 423 | + | ||
| 424 | + | // Reset file pointer to beginning | |
| 425 | + | if _, err := file.Seek(0, 0); err != nil { | |
| 426 | + | return fmt.Errorf("failed to reset file pointer: %w", err) | |
| 427 | + | } | |
| 428 | + | ||
| 429 | + | log.Debug("Chunk assembly completed successfully", | |
| 430 | + | "totalBytesWritten", written, | |
| 431 | + | "sizeInMB", fmt.Sprintf("%.2f MB", float64(written)/(1024*1024))) | |
| 432 | + | ||
| 433 | + | return nil | |
| 434 | + | } | |
| 435 | + | ||
| 436 | + | func getTransferData(transferID string) (*ChunkMetadata, map[int][]byte, error) { | |
| 437 | + | chunkStore.mu.Lock() | |
| 438 | + | defer chunkStore.mu.Unlock() | |
| 439 | + | ||
| 440 | + | metadata := chunkStore.metadata[transferID] | |
| 441 | + | if metadata == nil { | |
| 442 | + | return nil, nil, fmt.Errorf("transfer metadata not found") | |
| 443 | + | } | |
| 444 | + | return metadata, chunkStore.chunks[transferID], nil | |
| 445 | + | } | |
| @@ -2,9 +2,12 @@ | |||||
| 2 | package handlers | 2 | package handlers | ||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | + | "bytes" | |||
| 5 | "errors" | 6 | "errors" | ||
| 6 | "fmt" | 7 | "fmt" | ||
| 8 | + | "io" | |||
| 7 | "net/http" | 9 | "net/http" | ||
| 10 | + | "os" | |||
| 8 | "regexp" | 11 | "regexp" | ||
| 9 | "sync" | 12 | "sync" | ||
| 10 | "time" | 13 | "time" | ||
| @@ -100,19 +103,6 @@ func GetInfos(c echo.Context, a *server.App) error { | |||||
| 100 | return sendJSONResponse(c, http.StatusOK, info) | 103 | return sendJSONResponse(c, http.StatusOK, info) | ||
| 101 | } | 104 | } | ||
| 102 | 105 | ||||
| 103 | - | // validateAndPreparePushPayload validates and prepares the push payload | |||
| 104 | - | func validateAndPreparePushPayload(c echo.Context) (*common.PushPayload, error) { | |||
| 105 | - | payload := &common.PushPayload{ | |||
| 106 | - | ImageName: c.Request().Header.Get("X-Image-Name"), | |||
| 107 | - | } | |||
| 108 | - | ||||
| 109 | - | if payload.ImageName == "" { | |||
| 110 | - | return nil, errors.New("invalid image name") | |||
| 111 | - | } | |||
| 112 | - | ||||
| 113 | - | return payload, nil | |||
| 114 | - | } | |||
| 115 | - | ||||
| 116 | // validateAndPrepareDeployPayload validates and prepares the deploy payload | 106 | // validateAndPrepareDeployPayload validates and prepares the deploy payload | ||
| 117 | func validateAndPrepareDeployPayload(c echo.Context) (*common.DeployPayload, error) { | 107 | func validateAndPrepareDeployPayload(c echo.Context) (*common.DeployPayload, error) { | ||
| 118 | payload := &common.DeployPayload{ | 108 | payload := &common.DeployPayload{ | ||
| @@ -353,3 +343,103 @@ func extractContainerID(errorMessage string) string { | |||||
| 353 | func storeIDMapping(shortID, fullID string) { | 343 | func storeIDMapping(shortID, fullID string) { | ||
| 354 | safelyInteractWithIDMap(Update, shortID, fullID) | 344 | safelyInteractWithIDMap(Update, shortID, fullID) | ||
| 355 | } | 345 | } | ||
| 346 | + | ||||
| 347 | + | func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) error { | |||
| 348 | + | // Validate input parameters | |||
| 349 | + | if file == nil { | |||
| 350 | + | return errors.New("nil file handle provided") | |||
| 351 | + | } | |||
| 352 | + | if len(chunks) == 0 { | |||
| 353 | + | return errors.New("no chunks provided") | |||
| 354 | + | } | |||
| 355 | + | if totalChunks <= 0 { | |||
| 356 | + | return fmt.Errorf("invalid total chunks count: %d", totalChunks) | |||
| 357 | + | } | |||
| 358 | + | ||||
| 359 | + | // Verify we have all chunks before starting | |||
| 360 | + | if len(chunks) != totalChunks { | |||
| 361 | + | return fmt.Errorf("chunk count mismatch: got %d, expected %d", len(chunks), totalChunks) | |||
| 362 | + | } | |||
| 363 | + | ||||
| 364 | + | // Calculate total expected size | |||
| 365 | + | var totalSize int64 | |||
| 366 | + | for _, chunk := range chunks { | |||
| 367 | + | totalSize += int64(len(chunk)) | |||
| 368 | + | } | |||
| 369 | + | ||||
| 370 | + | log.Debug("Starting chunk assembly", | |||
| 371 | + | "totalChunks", totalChunks, | |||
| 372 | + | "totalSize", fmt.Sprintf("%.2f MB", float64(totalSize)/(1024*1024))) | |||
| 373 | + | ||||
| 374 | + | // First, seek to the beginning of the file | |||
| 375 | + | if _, err := file.Seek(0, 0); err != nil { | |||
| 376 | + | return fmt.Errorf("failed to seek to beginning: %w", err) | |||
| 377 | + | } | |||
| 378 | + | ||||
| 379 | + | // Create a temporary buffer with capacity hint | |||
| 380 | + | tempBuffer := bytes.NewBuffer(make([]byte, 0, totalSize)) | |||
| 381 | + | ||||
| 382 | + | // Write chunks in order with validation | |||
| 383 | + | for i := 0; i < totalChunks; i++ { | |||
| 384 | + | chunk, exists := chunks[i] | |||
| 385 | + | if !exists { | |||
| 386 | + | return fmt.Errorf("missing chunk %d", i) | |||
| 387 | + | } | |||
| 388 | + | ||||
| 389 | + | if len(chunk) == 0 { | |||
| 390 | + | return fmt.Errorf("empty chunk found at index %d", i) | |||
| 391 | + | } | |||
| 392 | + | ||||
| 393 | + | n, err := tempBuffer.Write(chunk) | |||
| 394 | + | if err != nil { | |||
| 395 | + | return fmt.Errorf("failed to write chunk %d to buffer: %w", i, err) | |||
| 396 | + | } | |||
| 397 | + | if n != len(chunk) { | |||
| 398 | + | return fmt.Errorf("incomplete chunk write: wrote %d of %d bytes for chunk %d", | |||
| 399 | + | n, len(chunk), i) | |||
| 400 | + | } | |||
| 401 | + | } | |||
| 402 | + | ||||
| 403 | + | // Verify buffer size matches expected total | |||
| 404 | + | if int64(tempBuffer.Len()) != totalSize { | |||
| 405 | + | return fmt.Errorf("buffer size mismatch: got %d, expected %d", | |||
| 406 | + | tempBuffer.Len(), totalSize) | |||
| 407 | + | } | |||
| 408 | + | ||||
| 409 | + | // Write the complete buffer to file | |||
| 410 | + | written, err := io.Copy(file, tempBuffer) | |||
| 411 | + | if err != nil { | |||
| 412 | + | return fmt.Errorf("failed to write buffer to file: %w", err) | |||
| 413 | + | } | |||
| 414 | + | if written != totalSize { | |||
| 415 | + | return fmt.Errorf("file write size mismatch: wrote %d, expected %d", | |||
| 416 | + | written, totalSize) | |||
| 417 | + | } | |||
| 418 | + | ||||
| 419 | + | // Ensure all data is written to disk | |||
| 420 | + | if err := file.Sync(); err != nil { | |||
| 421 | + | return fmt.Errorf("failed to sync file: %w", err) | |||
| 422 | + | } | |||
| 423 | + | ||||
| 424 | + | // Reset file pointer to beginning | |||
| 425 | + | if _, err := file.Seek(0, 0); err != nil { | |||
| 426 | + | return fmt.Errorf("failed to reset file pointer: %w", err) | |||
| 427 | + | } | |||
| 428 | + | ||||
| 429 | + | log.Debug("Chunk assembly completed successfully", | |||
| 430 | + | "totalBytesWritten", written, | |||
| 431 | + | "sizeInMB", fmt.Sprintf("%.2f MB", float64(written)/(1024*1024))) | |||
| 432 | + | ||||
| 433 | + | return nil | |||
| 434 | + | } | |||
| 435 | + | ||||
| 436 | + | func getTransferData(transferID string) (*ChunkMetadata, map[int][]byte, error) { | |||
| 437 | + | chunkStore.mu.Lock() | |||
| 438 | + | defer chunkStore.mu.Unlock() | |||
| 439 | + | ||||
| 440 | + | metadata := chunkStore.metadata[transferID] | |||
| 441 | + | if metadata == nil { | |||
| 442 | + | return nil, nil, fmt.Errorf("transfer metadata not found") | |||
| 443 | + | } | |||
| 444 | + | return metadata, chunkStore.chunks[transferID], nil | |||
| 445 | + | } | |||
M internal/httpserve/handlers/endpoint_push.go +98 -72
| @@ -2,20 +2,18 @@ | |||
| 2 | 2 | package handlers | |
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | - | "bytes" | |
| 6 | 5 | "encoding/json" | |
| 6 | + | "errors" | |
| 7 | 7 | "fmt" | |
| 8 | 8 | "io" | |
| 9 | 9 | "net/http" | |
| 10 | 10 | "os" | |
| 11 | 11 | "path/filepath" | |
| 12 | - | "runtime" | |
| 13 | 12 | "strings" | |
| 14 | 13 | ||
| 15 | 14 | "github.com/bnema/gordon/internal/common" | |
| 16 | 15 | "github.com/bnema/gordon/internal/server" | |
| 17 | 16 | "github.com/bnema/gordon/pkg/docker" | |
| 18 | - | "github.com/bnema/gordon/pkg/verify" | |
| 19 | 17 | "github.com/charmbracelet/log" | |
| 20 | 18 | "github.com/labstack/echo/v4" | |
| 21 | 19 | ) | |
| @@ -68,85 +66,134 @@ func handleChunkedPush(c echo.Context, a *server.App, metadataStr string) error | |||
| 68 | 66 | }) | |
| 69 | 67 | } | |
| 70 | 68 | ||
| 71 | - | // Store chunk data for multi-chunk transfers | |
| 69 | + | // Initialize chunk storage first | |
| 72 | 70 | chunkStore.mu.Lock() | |
| 73 | 71 | if chunkStore.chunks[metadata.TransferID] == nil { | |
| 74 | 72 | chunkStore.chunks[metadata.TransferID] = make(map[int][]byte) | |
| 75 | 73 | chunkStore.metadata[metadata.TransferID] = &metadata | |
| 76 | 74 | } | |
| 75 | + | chunkStore.mu.Unlock() | |
| 77 | 76 | ||
| 78 | 77 | chunkData, err := io.ReadAll(c.Request().Body) | |
| 79 | 78 | if err != nil { | |
| 80 | - | chunkStore.mu.Unlock() | |
| 81 | 79 | return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ | |
| 82 | 80 | Success: false, | |
| 83 | 81 | Message: "Failed to read chunk data", | |
| 84 | 82 | }) | |
| 85 | 83 | } | |
| 86 | 84 | ||
| 87 | - | // Store the chunk | |
| 85 | + | // Validate chunk data | |
| 86 | + | if err := validatePushChunk(chunkData, &metadata); err != nil { | |
| 87 | + | log.Error("Chunk validation failed", | |
| 88 | + | "error", err, | |
| 89 | + | "chunkNumber", metadata.ChunkNumber, | |
| 90 | + | "transferID", metadata.TransferID) | |
| 91 | + | return sendJSONResponse(c, http.StatusBadRequest, PushResponse{ | |
| 92 | + | Success: false, | |
| 93 | + | Message: fmt.Sprintf("Invalid chunk: %v", err), | |
| 94 | + | }) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // Store chunk data | |
| 98 | + | chunkStore.mu.Lock() | |
| 88 | 99 | chunkStore.chunks[metadata.TransferID][metadata.ChunkNumber] = chunkData | |
| 89 | 100 | chunkStore.mu.Unlock() | |
| 90 | 101 | ||
| 91 | - | // If this is the last chunk, process the complete transfer | |
| 102 | + | // Process complete transfer if this was the last chunk | |
| 92 | 103 | if isTransferComplete(metadata.TransferID) { | |
| 93 | 104 | return processCompleteChunkedPushTransfer(c, a, metadata.TransferID) | |
| 94 | 105 | } | |
| 95 | 106 | ||
| 96 | - | // For intermediate chunks, just acknowledge receipt | |
| 107 | + | // Acknowledge intermediate chunk | |
| 97 | 108 | return sendJSONResponse(c, http.StatusOK, PushResponse{ | |
| 98 | 109 | Success: true, | |
| 99 | - | Message: fmt.Sprintf("Chunk %d/%d received", metadata.ChunkNumber+1, metadata.TotalChunks), | |
| 110 | + | Message: fmt.Sprintf("Chunk %d/%d received successfully", | |
| 111 | + | metadata.ChunkNumber+1, metadata.TotalChunks), | |
| 100 | 112 | }) | |
| 101 | 113 | } | |
| 102 | 114 | ||
| 103 | 115 | func processCompleteChunkedPushTransfer(c echo.Context, a *server.App, transferID string) error { | |
| 104 | - | log.Info("Starting complete push transfer processing", "transferID", transferID) | |
| 116 | + | log.Info("Starting complete push transfer processing", | |
| 117 | + | "transferID", transferID) | |
| 105 | 118 | ||
| 106 | - | imageName := c.Request().Header.Get("X-Image-Name") | |
| 107 | 119 | // Get transfer data | |
| 108 | 120 | metadata, chunks, err := getTransferData(transferID) | |
| 109 | 121 | if err != nil { | |
| 110 | - | return sendErrorResponse(c, "Failed to get transfer data", err) | |
| 122 | + | return sendPushErrorResponse(c, "Failed to get transfer data", err) | |
| 111 | 123 | } | |
| 112 | 124 | ||
| 113 | - | // Create temporary file with .tar extension | |
| 114 | - | tmpDir, err := os.MkdirTemp("", "docker-import-*") | |
| 125 | + | // Validate we have all chunks | |
| 126 | + | if len(chunks) != metadata.TotalChunks { | |
| 127 | + | return sendPushErrorResponse(c, "Incomplete transfer", | |
| 128 | + | fmt.Errorf("expected %d chunks, got %d", metadata.TotalChunks, len(chunks))) | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // Create temporary directory with specific naming | |
| 132 | + | tmpDir, err := os.MkdirTemp("", "docker-push-*") | |
| 115 | 133 | if err != nil { | |
| 116 | - | return sendErrorResponse(c, "Failed to create temp directory", err) | |
| 134 | + | return sendPushErrorResponse(c, "Failed to create temp directory", err) | |
| 117 | 135 | } | |
| 118 | 136 | defer os.RemoveAll(tmpDir) | |
| 119 | 137 | ||
| 138 | + | // Create temporary file with .tar extension | |
| 120 | 139 | tmpFile, err := os.Create(filepath.Join(tmpDir, "image.tar")) | |
| 121 | 140 | if err != nil { | |
| 122 | - | return sendErrorResponse(c, "Failed to create temp file", err) | |
| 141 | + | return sendPushErrorResponse(c, "Failed to create temp file", err) | |
| 123 | 142 | } | |
| 124 | 143 | defer tmpFile.Close() | |
| 125 | 144 | ||
| 126 | - | // Write chunks to file | |
| 127 | - | if err := writeChunksToFile(tmpFile, chunks, metadata.TotalChunks); err != nil { | |
| 128 | - | return sendErrorResponse(c, "Failed to write chunks", err) | |
| 145 | + | // Write chunks in order to ensure proper tar assembly | |
| 146 | + | for i := 0; i < metadata.TotalChunks; i++ { | |
| 147 | + | chunk, exists := chunks[i] | |
| 148 | + | if !exists { | |
| 149 | + | return sendPushErrorResponse(c, "Missing chunk", | |
| 150 | + | fmt.Errorf("chunk %d not found", i)) | |
| 151 | + | } | |
| 152 | + | if _, err := tmpFile.Write(chunk); err != nil { | |
| 153 | + | return sendPushErrorResponse(c, "Failed to write chunk", | |
| 154 | + | fmt.Errorf("error writing chunk %d: %v", i, err)) | |
| 155 | + | } | |
| 129 | 156 | } | |
| 130 | 157 | ||
| 131 | - | // Verify the file is a valid tar archive | |
| 132 | - | if err := verify.VerifyTarFile(tmpFile.Name()); err != nil { | |
| 133 | - | return sendErrorResponse(c, "Invalid tar archive", err) | |
| 158 | + | // Ensure all data is written to disk | |
| 159 | + | if err := tmpFile.Sync(); err != nil { | |
| 160 | + | return sendPushErrorResponse(c, "Failed to sync file", err) | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | // Reset file pointer to beginning | |
| 164 | + | if _, err := tmpFile.Seek(0, 0); err != nil { | |
| 165 | + | return sendPushErrorResponse(c, "Failed to reset file pointer", err) | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // Verify file size | |
| 169 | + | fileInfo, err := tmpFile.Stat() | |
| 170 | + | if err != nil { | |
| 171 | + | return sendPushErrorResponse(c, "Failed to get file info", err) | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | log.Info("Image file created", | |
| 175 | + | "size", fmt.Sprintf("%.2f MB", float64(fileInfo.Size())/(1024*1024)), | |
| 176 | + | "path", tmpFile.Name()) | |
| 177 | + | ||
| 178 | + | imageName := c.Request().Header.Get("X-Image-Name") | |
| 179 | + | if imageName == "" { | |
| 180 | + | return sendPushErrorResponse(c, "Missing image name", fmt.Errorf("X-Image-Name header is required")) | |
| 134 | 181 | } | |
| 135 | 182 | ||
| 183 | + | log.Info("Importing image", | |
| 184 | + | "path", tmpFile.Name(), | |
| 185 | + | "imageName", imageName) | |
| 186 | + | ||
| 136 | 187 | // Import the image | |
| 137 | - | log.Info("Importing image from file", "path", tmpFile.Name()) | |
| 138 | 188 | imageID, err := docker.ImportImageToEngine(tmpFile.Name()) | |
| 139 | 189 | if err != nil { | |
| 140 | 190 | if strings.Contains(err.Error(), "platform compatibility check failed") { | |
| 141 | - | log.Error("Platform compatibility check failed", | |
| 142 | - | "error", err, | |
| 143 | - | "imageName", imageName) | |
| 144 | - | return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{ | |
| 191 | + | return sendJSONResponse(c, http.StatusBadRequest, PushResponse{ | |
| 145 | 192 | Success: false, | |
| 146 | - | Message: fmt.Sprintf("Image platform not compatible with server. Server is running on %s/%s. Please use a compatible image.", runtime.GOOS, runtime.GOARCH), | |
| 193 | + | Message: fmt.Sprintf("Image platform not compatible with server: %v", err), | |
| 147 | 194 | }) | |
| 148 | 195 | } | |
| 149 | - | return sendErrorResponse(c, "Failed to import image", err) | |
| 196 | + | return sendPushErrorResponse(c, "Failed to import image", err) | |
| 150 | 197 | } | |
| 151 | 198 | ||
| 152 | 199 | // Cleanup and prepare response | |
| @@ -163,61 +210,40 @@ func processCompleteChunkedPushTransfer(c echo.Context, a *server.App, transferI | |||
| 163 | 210 | }) | |
| 164 | 211 | } | |
| 165 | 212 | ||
| 166 | - | // Helper functions | |
| 167 | - | func getTransferData(transferID string) (*ChunkMetadata, map[int][]byte, error) { | |
| 168 | - | chunkStore.mu.Lock() | |
| 169 | - | defer chunkStore.mu.Unlock() | |
| 170 | - | ||
| 171 | - | metadata := chunkStore.metadata[transferID] | |
| 172 | - | if metadata == nil { | |
| 173 | - | return nil, nil, fmt.Errorf("transfer metadata not found") | |
| 174 | - | } | |
| 175 | - | return metadata, chunkStore.chunks[transferID], nil | |
| 176 | - | } | |
| 177 | - | ||
| 178 | - | func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) error { | |
| 179 | - | // First, seek to the beginning of the file | |
| 180 | - | if _, err := file.Seek(0, 0); err != nil { | |
| 181 | - | return fmt.Errorf("failed to seek to beginning: %w", err) | |
| 182 | - | } | |
| 183 | - | ||
| 184 | - | // Create a temporary buffer to hold all chunks | |
| 185 | - | tempBuffer := new(bytes.Buffer) | |
| 186 | - | ||
| 187 | - | // Write chunks in order | |
| 188 | - | for i := 0; i < totalChunks; i++ { | |
| 189 | - | chunk, exists := chunks[i] | |
| 190 | - | if !exists { | |
| 191 | - | return fmt.Errorf("missing chunk %d", i) | |
| 192 | - | } | |
| 193 | - | ||
| 194 | - | if _, err := tempBuffer.Write(chunk); err != nil { | |
| 195 | - | return fmt.Errorf("failed to write chunk %d to buffer: %w", i, err) | |
| 196 | - | } | |
| 197 | - | } | |
| 198 | - | ||
| 199 | - | // Write the complete buffer to file | |
| 200 | - | if _, err := io.Copy(file, tempBuffer); err != nil { | |
| 201 | - | return fmt.Errorf("failed to write buffer to file: %w", err) | |
| 213 | + | func validatePushChunk(chunk []byte, metadata *ChunkMetadata) error { | |
| 214 | + | if len(chunk) == 0 { | |
| 215 | + | return errors.New("empty chunk received") | |
| 202 | 216 | } | |
| 203 | 217 | ||
| 204 | - | // Ensure all data is written to disk | |
| 205 | - | if err := file.Sync(); err != nil { | |
| 206 | - | return fmt.Errorf("failed to sync file: %w", err) | |
| 218 | + | if metadata == nil { | |
| 219 | + | return errors.New("missing chunk metadata") | |
| 207 | 220 | } | |
| 208 | 221 | ||
| 209 | - | // Reset file pointer to beginning | |
| 210 | - | if _, err := file.Seek(0, 0); err != nil { | |
| 211 | - | return fmt.Errorf("failed to reset file pointer: %w", err) | |
| 222 | + | if int64(len(chunk)) != metadata.ChunkSize { | |
| 223 | + | return fmt.Errorf("chunk size mismatch: expected %d, got %d", | |
| 224 | + | metadata.ChunkSize, len(chunk)) | |
| 212 | 225 | } | |
| 213 | 226 | ||
| 214 | 227 | return nil | |
| 215 | 228 | } | |
| 216 | 229 | ||
| 217 | - | func sendErrorResponse(c echo.Context, message string, err error) error { | |
| 230 | + | func sendPushErrorResponse(c echo.Context, message string, err error) error { | |
| 218 | 231 | log.Error(message, "error", err) | |
| 219 | 232 | return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ | |
| 220 | 233 | Success: false, | |
| 221 | 234 | Message: message, | |
| 222 | 235 | }) | |
| 223 | 236 | } | |
| 237 | + | ||
| 238 | + | // validateAndPreparePushPayload validates and prepares the push payload | |
| 239 | + | func validateAndPreparePushPayload(c echo.Context) (*common.PushPayload, error) { | |
| 240 | + | payload := &common.PushPayload{ | |
| 241 | + | ImageName: c.Request().Header.Get("X-Image-Name"), | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | if payload.ImageName == "" { | |
| 245 | + | return nil, errors.New("invalid image name") | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | return payload, nil | |
| 249 | + | } | |
| @@ -2,20 +2,18 @@ | |||||
| 2 | package handlers | 2 | package handlers | ||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | - | "bytes" | |||
| 6 | "encoding/json" | 5 | "encoding/json" | ||
| 6 | + | "errors" | |||
| 7 | "fmt" | 7 | "fmt" | ||
| 8 | "io" | 8 | "io" | ||
| 9 | "net/http" | 9 | "net/http" | ||
| 10 | "os" | 10 | "os" | ||
| 11 | "path/filepath" | 11 | "path/filepath" | ||
| 12 | - | "runtime" | |||
| 13 | "strings" | 12 | "strings" | ||
| 14 | 13 | ||||
| 15 | "github.com/bnema/gordon/internal/common" | 14 | "github.com/bnema/gordon/internal/common" | ||
| 16 | "github.com/bnema/gordon/internal/server" | 15 | "github.com/bnema/gordon/internal/server" | ||
| 17 | "github.com/bnema/gordon/pkg/docker" | 16 | "github.com/bnema/gordon/pkg/docker" | ||
| 18 | - | "github.com/bnema/gordon/pkg/verify" | |||
| 19 | "github.com/charmbracelet/log" | 17 | "github.com/charmbracelet/log" | ||
| 20 | "github.com/labstack/echo/v4" | 18 | "github.com/labstack/echo/v4" | ||
| 21 | ) | 19 | ) | ||
| @@ -68,85 +66,134 @@ func handleChunkedPush(c echo.Context, a *server.App, metadataStr string) error | |||||
| 68 | }) | 66 | }) | ||
| 69 | } | 67 | } | ||
| 70 | 68 | ||||
| 71 | - | // Store chunk data for multi-chunk transfers | 69 | + | // Initialize chunk storage first |
| 72 | chunkStore.mu.Lock() | 70 | chunkStore.mu.Lock() | ||
| 73 | if chunkStore.chunks[metadata.TransferID] == nil { | 71 | if chunkStore.chunks[metadata.TransferID] == nil { | ||
| 74 | chunkStore.chunks[metadata.TransferID] = make(map[int][]byte) | 72 | chunkStore.chunks[metadata.TransferID] = make(map[int][]byte) | ||
| 75 | chunkStore.metadata[metadata.TransferID] = &metadata | 73 | chunkStore.metadata[metadata.TransferID] = &metadata | ||
| 76 | } | 74 | } | ||
| 75 | + | chunkStore.mu.Unlock() | |||
| 77 | 76 | ||||
| 78 | chunkData, err := io.ReadAll(c.Request().Body) | 77 | chunkData, err := io.ReadAll(c.Request().Body) | ||
| 79 | if err != nil { | 78 | if err != nil { | ||
| 80 | - | chunkStore.mu.Unlock() | |||
| 81 | return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ | 79 | return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ | ||
| 82 | Success: false, | 80 | Success: false, | ||
| 83 | Message: "Failed to read chunk data", | 81 | Message: "Failed to read chunk data", | ||
| 84 | }) | 82 | }) | ||
| 85 | } | 83 | } | ||
| 86 | 84 | ||||
| 87 | - | // Store the chunk | 85 | + | // Validate chunk data |
| 86 | + | if err := validatePushChunk(chunkData, &metadata); err != nil { | |||
| 87 | + | log.Error("Chunk validation failed", | |||
| 88 | + | "error", err, | |||
| 89 | + | "chunkNumber", metadata.ChunkNumber, | |||
| 90 | + | "transferID", metadata.TransferID) | |||
| 91 | + | return sendJSONResponse(c, http.StatusBadRequest, PushResponse{ | |||
| 92 | + | Success: false, | |||
| 93 | + | Message: fmt.Sprintf("Invalid chunk: %v", err), | |||
| 94 | + | }) | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // Store chunk data | |||
| 98 | + | chunkStore.mu.Lock() | |||
| 88 | chunkStore.chunks[metadata.TransferID][metadata.ChunkNumber] = chunkData | 99 | chunkStore.chunks[metadata.TransferID][metadata.ChunkNumber] = chunkData | ||
| 89 | chunkStore.mu.Unlock() | 100 | chunkStore.mu.Unlock() | ||
| 90 | 101 | ||||
| 91 | - | // If this is the last chunk, process the complete transfer | 102 | + | // Process complete transfer if this was the last chunk |
| 92 | if isTransferComplete(metadata.TransferID) { | 103 | if isTransferComplete(metadata.TransferID) { | ||
| 93 | return processCompleteChunkedPushTransfer(c, a, metadata.TransferID) | 104 | return processCompleteChunkedPushTransfer(c, a, metadata.TransferID) | ||
| 94 | } | 105 | } | ||
| 95 | 106 | ||||
| 96 | - | // For intermediate chunks, just acknowledge receipt | 107 | + | // Acknowledge intermediate chunk |
| 97 | return sendJSONResponse(c, http.StatusOK, PushResponse{ | 108 | return sendJSONResponse(c, http.StatusOK, PushResponse{ | ||
| 98 | Success: true, | 109 | Success: true, | ||
| 99 | - | Message: fmt.Sprintf("Chunk %d/%d received", metadata.ChunkNumber+1, metadata.TotalChunks), | 110 | + | Message: fmt.Sprintf("Chunk %d/%d received successfully", |
| 111 | + | metadata.ChunkNumber+1, metadata.TotalChunks), | |||
| 100 | }) | 112 | }) | ||
| 101 | } | 113 | } | ||
| 102 | 114 | ||||
| 103 | func processCompleteChunkedPushTransfer(c echo.Context, a *server.App, transferID string) error { | 115 | func processCompleteChunkedPushTransfer(c echo.Context, a *server.App, transferID string) error { | ||
| 104 | - | log.Info("Starting complete push transfer processing", "transferID", transferID) | 116 | + | log.Info("Starting complete push transfer processing", |
| 117 | + | "transferID", transferID) | |||
| 105 | 118 | ||||
| 106 | - | imageName := c.Request().Header.Get("X-Image-Name") | |||
| 107 | // Get transfer data | 119 | // Get transfer data | ||
| 108 | metadata, chunks, err := getTransferData(transferID) | 120 | metadata, chunks, err := getTransferData(transferID) | ||
| 109 | if err != nil { | 121 | if err != nil { | ||
| 110 | - | return sendErrorResponse(c, "Failed to get transfer data", err) | 122 | + | return sendPushErrorResponse(c, "Failed to get transfer data", err) |
| 111 | } | 123 | } | ||
| 112 | 124 | ||||
| 113 | - | // Create temporary file with .tar extension | 125 | + | // Validate we have all chunks |
| 114 | - | tmpDir, err := os.MkdirTemp("", "docker-import-*") | 126 | + | if len(chunks) != metadata.TotalChunks { |
| 127 | + | return sendPushErrorResponse(c, "Incomplete transfer", | |||
| 128 | + | fmt.Errorf("expected %d chunks, got %d", metadata.TotalChunks, len(chunks))) | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // Create temporary directory with specific naming | |||
| 132 | + | tmpDir, err := os.MkdirTemp("", "docker-push-*") | |||
| 115 | if err != nil { | 133 | if err != nil { | ||
| 116 | - | return sendErrorResponse(c, "Failed to create temp directory", err) | 134 | + | return sendPushErrorResponse(c, "Failed to create temp directory", err) |
| 117 | } | 135 | } | ||
| 118 | defer os.RemoveAll(tmpDir) | 136 | defer os.RemoveAll(tmpDir) | ||
| 119 | 137 | ||||
| 138 | + | // Create temporary file with .tar extension | |||
| 120 | tmpFile, err := os.Create(filepath.Join(tmpDir, "image.tar")) | 139 | tmpFile, err := os.Create(filepath.Join(tmpDir, "image.tar")) | ||
| 121 | if err != nil { | 140 | if err != nil { | ||
| 122 | - | return sendErrorResponse(c, "Failed to create temp file", err) | 141 | + | return sendPushErrorResponse(c, "Failed to create temp file", err) |
| 123 | } | 142 | } | ||
| 124 | defer tmpFile.Close() | 143 | defer tmpFile.Close() | ||
| 125 | 144 | ||||
| 126 | - | // Write chunks to file | 145 | + | // Write chunks in order to ensure proper tar assembly |
| 127 | - | if err := writeChunksToFile(tmpFile, chunks, metadata.TotalChunks); err != nil { | 146 | + | for i := 0; i < metadata.TotalChunks; i++ { |
| 128 | - | return sendErrorResponse(c, "Failed to write chunks", err) | 147 | + | chunk, exists := chunks[i] |
| 148 | + | if !exists { | |||
| 149 | + | return sendPushErrorResponse(c, "Missing chunk", | |||
| 150 | + | fmt.Errorf("chunk %d not found", i)) | |||
| 151 | + | } | |||
| 152 | + | if _, err := tmpFile.Write(chunk); err != nil { | |||
| 153 | + | return sendPushErrorResponse(c, "Failed to write chunk", | |||
| 154 | + | fmt.Errorf("error writing chunk %d: %v", i, err)) | |||
| 155 | + | } | |||
| 129 | } | 156 | } | ||
| 130 | 157 | ||||
| 131 | - | // Verify the file is a valid tar archive | 158 | + | // Ensure all data is written to disk |
| 132 | - | if err := verify.VerifyTarFile(tmpFile.Name()); err != nil { | 159 | + | if err := tmpFile.Sync(); err != nil { |
| 133 | - | return sendErrorResponse(c, "Invalid tar archive", err) | 160 | + | return sendPushErrorResponse(c, "Failed to sync file", err) |
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | // Reset file pointer to beginning | |||
| 164 | + | if _, err := tmpFile.Seek(0, 0); err != nil { | |||
| 165 | + | return sendPushErrorResponse(c, "Failed to reset file pointer", err) | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | // Verify file size | |||
| 169 | + | fileInfo, err := tmpFile.Stat() | |||
| 170 | + | if err != nil { | |||
| 171 | + | return sendPushErrorResponse(c, "Failed to get file info", err) | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | log.Info("Image file created", | |||
| 175 | + | "size", fmt.Sprintf("%.2f MB", float64(fileInfo.Size())/(1024*1024)), | |||
| 176 | + | "path", tmpFile.Name()) | |||
| 177 | + | ||||
| 178 | + | imageName := c.Request().Header.Get("X-Image-Name") | |||
| 179 | + | if imageName == "" { | |||
| 180 | + | return sendPushErrorResponse(c, "Missing image name", fmt.Errorf("X-Image-Name header is required")) | |||
| 134 | } | 181 | } | ||
| 135 | 182 | ||||
| 183 | + | log.Info("Importing image", | |||
| 184 | + | "path", tmpFile.Name(), | |||
| 185 | + | "imageName", imageName) | |||
| 186 | + | ||||
| 136 | // Import the image | 187 | // Import the image | ||
| 137 | - | log.Info("Importing image from file", "path", tmpFile.Name()) | |||
| 138 | imageID, err := docker.ImportImageToEngine(tmpFile.Name()) | 188 | imageID, err := docker.ImportImageToEngine(tmpFile.Name()) | ||
| 139 | if err != nil { | 189 | if err != nil { | ||
| 140 | if strings.Contains(err.Error(), "platform compatibility check failed") { | 190 | if strings.Contains(err.Error(), "platform compatibility check failed") { | ||
| 141 | - | log.Error("Platform compatibility check failed", | 191 | + | return sendJSONResponse(c, http.StatusBadRequest, PushResponse{ |
| 142 | - | "error", err, | |||
| 143 | - | "imageName", imageName) | |||
| 144 | - | return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{ | |||
| 145 | Success: false, | 192 | Success: false, | ||
| 146 | - | Message: fmt.Sprintf("Image platform not compatible with server. Server is running on %s/%s. Please use a compatible image.", runtime.GOOS, runtime.GOARCH), | 193 | + | Message: fmt.Sprintf("Image platform not compatible with server: %v", err), |
| 147 | }) | 194 | }) | ||
| 148 | } | 195 | } | ||
| 149 | - | return sendErrorResponse(c, "Failed to import image", err) | 196 | + | return sendPushErrorResponse(c, "Failed to import image", err) |
| 150 | } | 197 | } | ||
| 151 | 198 | ||||
| 152 | // Cleanup and prepare response | 199 | // Cleanup and prepare response | ||
| @@ -163,61 +210,40 @@ func processCompleteChunkedPushTransfer(c echo.Context, a *server.App, transferI | |||||
| 163 | }) | 210 | }) | ||
| 164 | } | 211 | } | ||
| 165 | 212 | ||||
| 166 | - | // Helper functions | 213 | + | func validatePushChunk(chunk []byte, metadata *ChunkMetadata) error { |
| 167 | - | func getTransferData(transferID string) (*ChunkMetadata, map[int][]byte, error) { | 214 | + | if len(chunk) == 0 { |
| 168 | - | chunkStore.mu.Lock() | 215 | + | return errors.New("empty chunk received") |
| 169 | - | defer chunkStore.mu.Unlock() | |||
| 170 | - | ||||
| 171 | - | metadata := chunkStore.metadata[transferID] | |||
| 172 | - | if metadata == nil { | |||
| 173 | - | return nil, nil, fmt.Errorf("transfer metadata not found") | |||
| 174 | - | } | |||
| 175 | - | return metadata, chunkStore.chunks[transferID], nil | |||
| 176 | - | } | |||
| 177 | - | ||||
| 178 | - | func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) error { | |||
| 179 | - | // First, seek to the beginning of the file | |||
| 180 | - | if _, err := file.Seek(0, 0); err != nil { | |||
| 181 | - | return fmt.Errorf("failed to seek to beginning: %w", err) | |||
| 182 | - | } | |||
| 183 | - | ||||
| 184 | - | // Create a temporary buffer to hold all chunks | |||
| 185 | - | tempBuffer := new(bytes.Buffer) | |||
| 186 | - | ||||
| 187 | - | // Write chunks in order | |||
| 188 | - | for i := 0; i < totalChunks; i++ { | |||
| 189 | - | chunk, exists := chunks[i] | |||
| 190 | - | if !exists { | |||
| 191 | - | return fmt.Errorf("missing chunk %d", i) | |||
| 192 | - | } | |||
| 193 | - | ||||
| 194 | - | if _, err := tempBuffer.Write(chunk); err != nil { | |||
| 195 | - | return fmt.Errorf("failed to write chunk %d to buffer: %w", i, err) | |||
| 196 | - | } | |||
| 197 | - | } | |||
| 198 | - | ||||
| 199 | - | // Write the complete buffer to file | |||
| 200 | - | if _, err := io.Copy(file, tempBuffer); err != nil { | |||
| 201 | - | return fmt.Errorf("failed to write buffer to file: %w", err) | |||
| 202 | } | 216 | } | ||
| 203 | 217 | ||||
| 204 | - | // Ensure all data is written to disk | 218 | + | if metadata == nil { |
| 205 | - | if err := file.Sync(); err != nil { | 219 | + | return errors.New("missing chunk metadata") |
| 206 | - | return fmt.Errorf("failed to sync file: %w", err) | |||
| 207 | } | 220 | } | ||
| 208 | 221 | ||||
| 209 | - | // Reset file pointer to beginning | 222 | + | if int64(len(chunk)) != metadata.ChunkSize { |
| 210 | - | if _, err := file.Seek(0, 0); err != nil { | 223 | + | return fmt.Errorf("chunk size mismatch: expected %d, got %d", |
| 211 | - | return fmt.Errorf("failed to reset file pointer: %w", err) | 224 | + | metadata.ChunkSize, len(chunk)) |
| 212 | } | 225 | } | ||
| 213 | 226 | ||||
| 214 | return nil | 227 | return nil | ||
| 215 | } | 228 | } | ||
| 216 | 229 | ||||
| 217 | - | func sendErrorResponse(c echo.Context, message string, err error) error { | 230 | + | func sendPushErrorResponse(c echo.Context, message string, err error) error { |
| 218 | log.Error(message, "error", err) | 231 | log.Error(message, "error", err) | ||
| 219 | return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ | 232 | return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ | ||
| 220 | Success: false, | 233 | Success: false, | ||
| 221 | Message: message, | 234 | Message: message, | ||
| 222 | }) | 235 | }) | ||
| 223 | } | 236 | } | ||
| 237 | + | ||||
| 238 | + | // validateAndPreparePushPayload validates and prepares the push payload | |||
| 239 | + | func validateAndPreparePushPayload(c echo.Context) (*common.PushPayload, error) { | |||
| 240 | + | payload := &common.PushPayload{ | |||
| 241 | + | ImageName: c.Request().Header.Get("X-Image-Name"), | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | if payload.ImageName == "" { | |||
| 245 | + | return nil, errors.New("invalid image name") | |||
| 246 | + | } | |||
| 247 | + | ||||
| 248 | + | return payload, nil | |||
| 249 | + | } | |||