detached bd2cd5e

fix: resolve push command corrupted image import and unify command structure

commit
bd2cd5eea206916a5268181c652aa7436403d9a7
author
Brice · · 2024-11-19 20:15 UTC
committer
Brice · · 2024-11-19 17:33 UTC
parent
cd319cd
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 {
4444 imageName := args[0]
4545 log.Info("Pushing image", "image", imageName)
4646
47- if err := validateInputs(imageName, port, targetDomain); err != nil {
47+ if err := validateDeployInputs(imageName, port, targetDomain); err != nil {
4848 log.Error("Validation failed", "error", err)
4949 return
5050 }
@@ -208,7 +208,7 @@ func waitForDeployment(domain string, containerID string) error {
208208 return fmt.Errorf("deployment not ready after %d attempts", maxRetries)
209209 }
210210
211-func validateInputs(imageName, port, targetDomain string) error {
211+func validateDeployInputs(imageName, port, targetDomain string) error {
212212 if err := handler.ValidateImageName(imageName); err != nil {
213213 return err
214214 }
@@ -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 return49 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 err213 return err
214 }214 }
M internal/cli/cmd/push.go +44 -58
@@ -1,17 +1,19 @@
11 package cmd
22
33 import (
4+ "bytes"
45 "context"
56 "encoding/json"
67 "fmt"
8+ "io"
79 "net/http"
810 "os"
911 "strings"
12+ "time"
1013
1114 "github.com/bnema/gordon/internal/cli"
1215 "github.com/bnema/gordon/internal/cli/handler"
1316 "github.com/bnema/gordon/internal/common"
14- "github.com/bnema/gordon/pkg/docker"
1517 "github.com/charmbracelet/log"
1618 "github.com/spf13/cobra"
1719 )
@@ -38,8 +40,23 @@ func NewPushCommand(a *cli.App) *cobra.Command {
3840 imageName := args[0]
3941 log.Info("Pushing image", "image", imageName)
4042
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 {
4360 os.Exit(1)
4461 }
4562 },
@@ -50,74 +67,36 @@ func NewPushCommand(a *cli.App) *cobra.Command {
5067 return pushCmd
5168 }
5269
53-func pushImage(a *cli.App, imageName string) error {
70+func pushImage(a *cli.App, reader io.Reader, imageName string) error {
5471 // Check authentication early
5572 if err := handler.CheckAndRefreshAuth(a); err != nil {
5673 log.Error("Authentication check failed", "error", err)
5774 return err
5875 }
5976
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)
7379 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)
7581 }
7682
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...",
7986 "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+ )
11689
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"},
11993 }
12094
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)
121100 var pushResponse common.PushResponse
122101 if err := json.Unmarshal(resp.Body, &pushResponse); err != nil {
123102 log.Error("Error parsing response", "error", err)
@@ -139,3 +118,10 @@ func pushImage(a *cli.App, imageName string) error {
139118
140119 return nil
141120 }
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 cmd1 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 pushCmd67 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 early71 // 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 err74 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 / 102483+ 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.PushResponse100 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 nil119 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
164164 })
165165 }
166166
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+
167188 // Store the complete chunk
168189 chunkStore.chunks[metadata.TransferID][metadata.ChunkNumber] = chunkData
169190 chunkStore.mu.Unlock()
@@ -188,25 +209,25 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe
188209 // Get transfer data
189210 metadata, chunks, err := getTransferData(transferID)
190211 if err != nil {
191- return sendErrorResponse(c, "Failed to get transfer data", err)
212+ return sendDeployErrorResponse(c, "Failed to get transfer data", err)
192213 }
193214
194215 // Create temporary directory for processing
195216 tmpDir, err := os.MkdirTemp("", "docker-deploy-*")
196217 if err != nil {
197- return sendErrorResponse(c, "Failed to create temp directory", err)
218+ return sendDeployErrorResponse(c, "Failed to create temp directory", err)
198219 }
199220 defer os.RemoveAll(tmpDir)
200221
201222 tmpFile, err := os.Create(filepath.Join(tmpDir, "image.tar"))
202223 if err != nil {
203- return sendErrorResponse(c, "Failed to create temp file", err)
224+ return sendDeployErrorResponse(c, "Failed to create temp file", err)
204225 }
205226 defer tmpFile.Close()
206227
207228 // Write chunks to file
208229 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)
210231 }
211232
212233 // Get deployment-specific headers
@@ -215,7 +236,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe
215236 imageName := c.Request().Header.Get("X-Image-Name")
216237
217238 if port == "" || targetDomain == "" || imageName == "" {
218- return sendErrorResponse(c, "Missing required deployment parameters",
239+ return sendDeployErrorResponse(c, "Missing required deployment parameters",
219240 fmt.Errorf("port: %s, domain: %s, image: %s", port, targetDomain, imageName))
220241 }
221242
@@ -224,7 +245,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe
224245 containerName := strings.Split(cleanDomain, ".")[0]
225246
226247 if containerName == "" {
227- return sendErrorResponse(c, "Invalid target domain",
248+ return sendDeployErrorResponse(c, "Invalid target domain",
228249 fmt.Errorf("couldn't extract container name from domain: %s", targetDomain))
229250 }
230251
@@ -259,7 +280,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe
259280 RawResponse: err.Error(),
260281 })
261282 }
262- return sendErrorResponse(c, "Failed to import image", err)
283+ return sendDeployErrorResponse(c, "Failed to import image", err)
263284 }
264285
265286 // Prepare deployment payload
@@ -285,7 +306,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe
285306 })
286307 }
287308
288- return sendErrorResponse(c, "Failed to create or start container", err)
309+ return sendDeployErrorResponse(c, "Failed to create or start container", err)
289310 }
290311
291312 // Cleanup transfer data
@@ -305,6 +326,14 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe
305326 })
306327 }
307328
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+
308337 // func waitForDeployment(domain string, containerID string) error {
309338 // client := &http.Client{Timeout: 10 * time.Second}
310339 // 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 chunk188 // Store the complete chunk
168 chunkStore.chunks[metadata.TransferID][metadata.ChunkNumber] = chunkData189 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 data209 // 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 processing215 // 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 file228 // 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 headers233 // 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 payload286 // 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 data312 // 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 := 10339 // maxRetries := 10
M internal/httpserve/handlers/endpoint_helpers.go +103 -13
@@ -2,9 +2,12 @@
22 package handlers
33
44 import (
5+ "bytes"
56 "errors"
67 "fmt"
8+ "io"
79 "net/http"
10+ "os"
811 "regexp"
912 "sync"
1013 "time"
@@ -100,19 +103,6 @@ func GetInfos(c echo.Context, a *server.App) error {
100103 return sendJSONResponse(c, http.StatusOK, info)
101104 }
102105
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-
116106 // validateAndPrepareDeployPayload validates and prepares the deploy payload
117107 func validateAndPrepareDeployPayload(c echo.Context) (*common.DeployPayload, error) {
118108 payload := &common.DeployPayload{
@@ -353,3 +343,103 @@ func extractContainerID(errorMessage string) string {
353343 func storeIDMapping(shortID, fullID string) {
354344 safelyInteractWithIDMap(Update, shortID, fullID)
355345 }
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 handlers2 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 payload106 // 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 @@
22 package handlers
33
44 import (
5- "bytes"
65 "encoding/json"
6+ "errors"
77 "fmt"
88 "io"
99 "net/http"
1010 "os"
1111 "path/filepath"
12- "runtime"
1312 "strings"
1413
1514 "github.com/bnema/gordon/internal/common"
1615 "github.com/bnema/gordon/internal/server"
1716 "github.com/bnema/gordon/pkg/docker"
18- "github.com/bnema/gordon/pkg/verify"
1917 "github.com/charmbracelet/log"
2018 "github.com/labstack/echo/v4"
2119 )
@@ -68,85 +66,134 @@ func handleChunkedPush(c echo.Context, a *server.App, metadataStr string) error
6866 })
6967 }
7068
71- // Store chunk data for multi-chunk transfers
69+ // Initialize chunk storage first
7270 chunkStore.mu.Lock()
7371 if chunkStore.chunks[metadata.TransferID] == nil {
7472 chunkStore.chunks[metadata.TransferID] = make(map[int][]byte)
7573 chunkStore.metadata[metadata.TransferID] = &metadata
7674 }
75+ chunkStore.mu.Unlock()
7776
7877 chunkData, err := io.ReadAll(c.Request().Body)
7978 if err != nil {
80- chunkStore.mu.Unlock()
8179 return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
8280 Success: false,
8381 Message: "Failed to read chunk data",
8482 })
8583 }
8684
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()
8899 chunkStore.chunks[metadata.TransferID][metadata.ChunkNumber] = chunkData
89100 chunkStore.mu.Unlock()
90101
91- // If this is the last chunk, process the complete transfer
102+ // Process complete transfer if this was the last chunk
92103 if isTransferComplete(metadata.TransferID) {
93104 return processCompleteChunkedPushTransfer(c, a, metadata.TransferID)
94105 }
95106
96- // For intermediate chunks, just acknowledge receipt
107+ // Acknowledge intermediate chunk
97108 return sendJSONResponse(c, http.StatusOK, PushResponse{
98109 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),
100112 })
101113 }
102114
103115 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)
105118
106- imageName := c.Request().Header.Get("X-Image-Name")
107119 // Get transfer data
108120 metadata, chunks, err := getTransferData(transferID)
109121 if err != nil {
110- return sendErrorResponse(c, "Failed to get transfer data", err)
122+ return sendPushErrorResponse(c, "Failed to get transfer data", err)
111123 }
112124
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-*")
115133 if err != nil {
116- return sendErrorResponse(c, "Failed to create temp directory", err)
134+ return sendPushErrorResponse(c, "Failed to create temp directory", err)
117135 }
118136 defer os.RemoveAll(tmpDir)
119137
138+ // Create temporary file with .tar extension
120139 tmpFile, err := os.Create(filepath.Join(tmpDir, "image.tar"))
121140 if err != nil {
122- return sendErrorResponse(c, "Failed to create temp file", err)
141+ return sendPushErrorResponse(c, "Failed to create temp file", err)
123142 }
124143 defer tmpFile.Close()
125144
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+ }
129156 }
130157
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"))
134181 }
135182
183+ log.Info("Importing image",
184+ "path", tmpFile.Name(),
185+ "imageName", imageName)
186+
136187 // Import the image
137- log.Info("Importing image from file", "path", tmpFile.Name())
138188 imageID, err := docker.ImportImageToEngine(tmpFile.Name())
139189 if err != nil {
140190 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{
145192 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),
147194 })
148195 }
149- return sendErrorResponse(c, "Failed to import image", err)
196+ return sendPushErrorResponse(c, "Failed to import image", err)
150197 }
151198
152199 // Cleanup and prepare response
@@ -163,61 +210,40 @@ func processCompleteChunkedPushTransfer(c echo.Context, a *server.App, transferI
163210 })
164211 }
165212
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")
202216 }
203217
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")
207220 }
208221
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))
212225 }
213226
214227 return nil
215228 }
216229
217-func sendErrorResponse(c echo.Context, message string, err error) error {
230+func sendPushErrorResponse(c echo.Context, message string, err error) error {
218231 log.Error(message, "error", err)
219232 return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
220233 Success: false,
221234 Message: message,
222235 })
223236 }
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 handlers2 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 transfers69+ // 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] = &metadata73 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 chunk85+ // 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] = chunkData99 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 transfer102+ // 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 receipt107+ // 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 data119 // 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 extension125+ // 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 file145+ // 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 archive158+ // 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 image187 // 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 response199 // 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 functions213+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 disk218+ 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 beginning222+ 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 nil227 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close