refactor: improve code quality and fix linting issues
- commit
- cccb2521db27b3e00ad73ab12e3b177ba6cff0ac
- author
- Brice · · 2024-11-18 08:19 UTC
- parent
- 9f77440
- signature
- unsigned
15 files · +316 -294
M .golangci.yml +0 -1
| @@ -15,7 +15,6 @@ linters: | |||
| 15 | 15 | - misspell | |
| 16 | 16 | - gosec | |
| 17 | 17 | - unconvert | |
| 18 | - | - deadcode | |
| 19 | 18 | - gocyclo | |
| 20 | 19 | ||
| 21 | 20 | linters-settings: | |
| @@ -15,7 +15,6 @@ linters: | |||||
| 15 | - misspell | 15 | - misspell | ||
| 16 | - gosec | 16 | - gosec | ||
| 17 | - unconvert | 17 | - unconvert | ||
| 18 | - | - deadcode | |||
| 19 | - gocyclo | 18 | - gocyclo | ||
| 20 | 19 | ||||
| 21 | linters-settings: | 20 | linters-settings: | ||
M internal/cli/cmd/deploy.go +12 -27
| @@ -86,8 +86,12 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str | |||
| 86 | 86 | return fmt.Errorf("conflict check failed: %w", err) | |
| 87 | 87 | } | |
| 88 | 88 | ||
| 89 | - | // Shorten the ContainerID to 12 characters | |
| 90 | - | shortID := conflictResp.ContainerID[:12] | |
| 89 | + | var shortID string | |
| 90 | + | if len(conflictResp.ContainerID) >= 12 { | |
| 91 | + | shortID = conflictResp.ContainerID[:12] | |
| 92 | + | } else { | |
| 93 | + | shortID = conflictResp.ContainerID // Use full ID if less than 12 chars | |
| 94 | + | } | |
| 91 | 95 | ||
| 92 | 96 | // If there's a conflict, handle it before proceeding | |
| 93 | 97 | if !conflictResp.Success && conflictResp.ContainerID != "" { | |
| @@ -141,7 +145,9 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str | |||
| 141 | 145 | if deployResponse.ContainerID != "" { | |
| 142 | 146 | log.Warn("Container already exists") | |
| 143 | 147 | // Retry the deployment after handling the conflict | |
| 144 | - | imageReader.Seek(0, 0) // Reset reader to beginning | |
| 148 | + | if _, err := imageReader.Seek(0, 0); err != nil { | |
| 149 | + | return fmt.Errorf("failed to reset image reader: %w", err) | |
| 150 | + | } | |
| 145 | 151 | resp, err = chunkedClient.SendFileAsChunks(ctx, "/deploy", headers, imageReader, size, imageName) | |
| 146 | 152 | if err != nil { | |
| 147 | 153 | return fmt.Errorf("deployment failed after container removal: %w", err) | |
| @@ -207,7 +213,9 @@ func validateInputs(imageName, port, targetDomain string) error { | |||
| 207 | 213 | return err | |
| 208 | 214 | } | |
| 209 | 215 | ||
| 210 | - | handler.EnsureImageTag(&imageName) | |
| 216 | + | if err := handler.EnsureImageTag(&imageName); err != nil { | |
| 217 | + | return fmt.Errorf("failed to ensure image tag: %w", err) | |
| 218 | + | } | |
| 211 | 219 | ||
| 212 | 220 | if port == "" { | |
| 213 | 221 | return fmt.Errorf("port is required") | |
| @@ -216,29 +224,6 @@ func validateInputs(imageName, port, targetDomain string) error { | |||
| 216 | 224 | return handler.ValidateTargetDomain(targetDomain) | |
| 217 | 225 | } | |
| 218 | 226 | ||
| 219 | - | func handleDeployError(a *cli.App, err error, imageName, port, targetDomain string, reader io.Reader) error { | |
| 220 | - | var deployErr *common.DeploymentError | |
| 221 | - | if errors.As(err, &deployErr) { | |
| 222 | - | var deployResponse common.DeployResponse | |
| 223 | - | var conflictResp common.ConflictCheckResponse | |
| 224 | - | if jsonErr := json.Unmarshal([]byte(deployErr.RawResponse), &deployResponse); jsonErr == nil { | |
| 225 | - | if deployErr.StatusCode == http.StatusConflict && deployResponse.ContainerID != "" { | |
| 226 | - | if err := handler.HandleExistingContainer(a, &conflictResp); err != nil { | |
| 227 | - | if strings.Contains(err.Error(), "cancelled by user") { | |
| 228 | - | log.Warn("Deployment cancelled by user") | |
| 229 | - | return nil | |
| 230 | - | } | |
| 231 | - | log.Error("Failed to handle existing container", "error", err) | |
| 232 | - | return err | |
| 233 | - | } | |
| 234 | - | // Retry deployment | |
| 235 | - | return deployImage(a, reader, imageName, port, targetDomain) | |
| 236 | - | } | |
| 237 | - | } | |
| 238 | - | } | |
| 239 | - | return err | |
| 240 | - | } | |
| 241 | - | ||
| 242 | 227 | func checkDeployConflict(a *cli.App, targetDomain string, port string) (*common.ConflictCheckResponse, error) { | |
| 243 | 228 | ||
| 244 | 229 | // Create request to check-conflict endpoint with both domain and port parameters | |
| @@ -86,8 +86,12 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str | |||||
| 86 | return fmt.Errorf("conflict check failed: %w", err) | 86 | return fmt.Errorf("conflict check failed: %w", err) | ||
| 87 | } | 87 | } | ||
| 88 | 88 | ||||
| 89 | - | // Shorten the ContainerID to 12 characters | 89 | + | var shortID string |
| 90 | - | shortID := conflictResp.ContainerID[:12] | 90 | + | if len(conflictResp.ContainerID) >= 12 { |
| 91 | + | shortID = conflictResp.ContainerID[:12] | |||
| 92 | + | } else { | |||
| 93 | + | shortID = conflictResp.ContainerID // Use full ID if less than 12 chars | |||
| 94 | + | } | |||
| 91 | 95 | ||||
| 92 | // If there's a conflict, handle it before proceeding | 96 | // If there's a conflict, handle it before proceeding | ||
| 93 | if !conflictResp.Success && conflictResp.ContainerID != "" { | 97 | if !conflictResp.Success && conflictResp.ContainerID != "" { | ||
| @@ -141,7 +145,9 @@ func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain str | |||||
| 141 | if deployResponse.ContainerID != "" { | 145 | if deployResponse.ContainerID != "" { | ||
| 142 | log.Warn("Container already exists") | 146 | log.Warn("Container already exists") | ||
| 143 | // Retry the deployment after handling the conflict | 147 | // Retry the deployment after handling the conflict | ||
| 144 | - | imageReader.Seek(0, 0) // Reset reader to beginning | 148 | + | if _, err := imageReader.Seek(0, 0); err != nil { |
| 149 | + | return fmt.Errorf("failed to reset image reader: %w", err) | |||
| 150 | + | } | |||
| 145 | resp, err = chunkedClient.SendFileAsChunks(ctx, "/deploy", headers, imageReader, size, imageName) | 151 | resp, err = chunkedClient.SendFileAsChunks(ctx, "/deploy", headers, imageReader, size, imageName) | ||
| 146 | if err != nil { | 152 | if err != nil { | ||
| 147 | return fmt.Errorf("deployment failed after container removal: %w", err) | 153 | return fmt.Errorf("deployment failed after container removal: %w", err) | ||
| @@ -207,7 +213,9 @@ func validateInputs(imageName, port, targetDomain string) error { | |||||
| 207 | return err | 213 | return err | ||
| 208 | } | 214 | } | ||
| 209 | 215 | ||||
| 210 | - | handler.EnsureImageTag(&imageName) | 216 | + | if err := handler.EnsureImageTag(&imageName); err != nil { |
| 217 | + | return fmt.Errorf("failed to ensure image tag: %w", err) | |||
| 218 | + | } | |||
| 211 | 219 | ||||
| 212 | if port == "" { | 220 | if port == "" { | ||
| 213 | return fmt.Errorf("port is required") | 221 | return fmt.Errorf("port is required") | ||
| @@ -216,29 +224,6 @@ func validateInputs(imageName, port, targetDomain string) error { | |||||
| 216 | return handler.ValidateTargetDomain(targetDomain) | 224 | return handler.ValidateTargetDomain(targetDomain) | ||
| 217 | } | 225 | } | ||
| 218 | 226 | ||||
| 219 | - | func handleDeployError(a *cli.App, err error, imageName, port, targetDomain string, reader io.Reader) error { | |||
| 220 | - | var deployErr *common.DeploymentError | |||
| 221 | - | if errors.As(err, &deployErr) { | |||
| 222 | - | var deployResponse common.DeployResponse | |||
| 223 | - | var conflictResp common.ConflictCheckResponse | |||
| 224 | - | if jsonErr := json.Unmarshal([]byte(deployErr.RawResponse), &deployResponse); jsonErr == nil { | |||
| 225 | - | if deployErr.StatusCode == http.StatusConflict && deployResponse.ContainerID != "" { | |||
| 226 | - | if err := handler.HandleExistingContainer(a, &conflictResp); err != nil { | |||
| 227 | - | if strings.Contains(err.Error(), "cancelled by user") { | |||
| 228 | - | log.Warn("Deployment cancelled by user") | |||
| 229 | - | return nil | |||
| 230 | - | } | |||
| 231 | - | log.Error("Failed to handle existing container", "error", err) | |||
| 232 | - | return err | |||
| 233 | - | } | |||
| 234 | - | // Retry deployment | |||
| 235 | - | return deployImage(a, reader, imageName, port, targetDomain) | |||
| 236 | - | } | |||
| 237 | - | } | |||
| 238 | - | } | |||
| 239 | - | return err | |||
| 240 | - | } | |||
| 241 | - | ||||
| 242 | func checkDeployConflict(a *cli.App, targetDomain string, port string) (*common.ConflictCheckResponse, error) { | 227 | func checkDeployConflict(a *cli.App, targetDomain string, port string) (*common.ConflictCheckResponse, error) { | ||
| 243 | 228 | ||||
| 244 | // Create request to check-conflict endpoint with both domain and port parameters | 229 | // Create request to check-conflict endpoint with both domain and port parameters | ||
M internal/cli/handler/chunked_transfer.go +15 -22
| @@ -38,14 +38,13 @@ type UploadProgress struct { | |||
| 38 | 38 | ||
| 39 | 39 | // ProgressDisplay manages the upload progress display | |
| 40 | 40 | type ProgressDisplay struct { | |
| 41 | - | mu sync.Mutex | |
| 42 | - | model *uploadModel | |
| 43 | - | program *tea.Program | |
| 44 | - | updateChan chan UploadProgress | |
| 45 | - | done chan struct{} | |
| 46 | - | interrupted bool | |
| 47 | - | paused bool | |
| 48 | - | pauseChan chan bool | |
| 41 | + | mu sync.Mutex | |
| 42 | + | model *uploadModel | |
| 43 | + | program *tea.Program | |
| 44 | + | updateChan chan UploadProgress | |
| 45 | + | done chan struct{} | |
| 46 | + | paused bool | |
| 47 | + | pauseChan chan bool | |
| 49 | 48 | } | |
| 50 | 49 | ||
| 51 | 50 | type uploadModel struct { | |
| @@ -80,7 +79,7 @@ func (pd *ProgressDisplay) Start() { | |||
| 80 | 79 | ||
| 81 | 80 | // Run the UI in a goroutine | |
| 82 | 81 | go func() { | |
| 83 | - | if err := pd.program.Start(); err != nil { | |
| 82 | + | if _, err := pd.program.Run(); err != nil { | |
| 84 | 83 | fmt.Printf("Error running progress display: %v\n", err) | |
| 85 | 84 | } | |
| 86 | 85 | }() | |
| @@ -199,14 +198,13 @@ func (m *uploadModel) View() string { | |||
| 199 | 198 | type ChunkMetadata = common.ChunkMetadata | |
| 200 | 199 | ||
| 201 | 200 | type ChunkedClient struct { | |
| 202 | - | client *http.Client | |
| 203 | - | baseURL string | |
| 204 | - | token string | |
| 205 | - | chunkSize int64 | |
| 206 | - | app *cli.App | |
| 207 | - | progress *ProgressDisplay | |
| 208 | - | cancelFunc context.CancelFunc | |
| 209 | - | conflictAction chan common.ConflictAction | |
| 201 | + | client *http.Client | |
| 202 | + | baseURL string | |
| 203 | + | token string | |
| 204 | + | chunkSize int64 | |
| 205 | + | app *cli.App | |
| 206 | + | progress *ProgressDisplay | |
| 207 | + | cancelFunc context.CancelFunc | |
| 210 | 208 | } | |
| 211 | 209 | ||
| 212 | 210 | type transferStore struct { | |
| @@ -562,11 +560,6 @@ func verifyChunkIntegrity(chunk []byte, metadata common.ChunkMetadata) error { | |||
| 562 | 560 | return nil | |
| 563 | 561 | } | |
| 564 | 562 | ||
| 565 | - | // isAuthError checks if a response is an authentication error | |
| 566 | - | func isAuthError(resp *Response) bool { | |
| 567 | - | return resp != nil && resp.StatusCode == http.StatusUnauthorized | |
| 568 | - | } | |
| 569 | - | ||
| 570 | 563 | // cleanupClientTransfer cleans up a client transfer | |
| 571 | 564 | func cleanupClientTransfer(transferID string) { | |
| 572 | 565 | clientChunkStore.mu.Lock() | |
| @@ -38,14 +38,13 @@ type UploadProgress struct { | |||||
| 38 | 38 | ||||
| 39 | // ProgressDisplay manages the upload progress display | 39 | // ProgressDisplay manages the upload progress display | ||
| 40 | type ProgressDisplay struct { | 40 | type ProgressDisplay struct { | ||
| 41 | - | mu sync.Mutex | 41 | + | mu sync.Mutex |
| 42 | - | model *uploadModel | 42 | + | model *uploadModel |
| 43 | - | program *tea.Program | 43 | + | program *tea.Program |
| 44 | - | updateChan chan UploadProgress | 44 | + | updateChan chan UploadProgress |
| 45 | - | done chan struct{} | 45 | + | done chan struct{} |
| 46 | - | interrupted bool | 46 | + | paused bool |
| 47 | - | paused bool | 47 | + | pauseChan chan bool |
| 48 | - | pauseChan chan bool | |||
| 49 | } | 48 | } | ||
| 50 | 49 | ||||
| 51 | type uploadModel struct { | 50 | type uploadModel struct { | ||
| @@ -80,7 +79,7 @@ func (pd *ProgressDisplay) Start() { | |||||
| 80 | 79 | ||||
| 81 | // Run the UI in a goroutine | 80 | // Run the UI in a goroutine | ||
| 82 | go func() { | 81 | go func() { | ||
| 83 | - | if err := pd.program.Start(); err != nil { | 82 | + | if _, err := pd.program.Run(); err != nil { |
| 84 | fmt.Printf("Error running progress display: %v\n", err) | 83 | fmt.Printf("Error running progress display: %v\n", err) | ||
| 85 | } | 84 | } | ||
| 86 | }() | 85 | }() | ||
| @@ -199,14 +198,13 @@ func (m *uploadModel) View() string { | |||||
| 199 | type ChunkMetadata = common.ChunkMetadata | 198 | type ChunkMetadata = common.ChunkMetadata | ||
| 200 | 199 | ||||
| 201 | type ChunkedClient struct { | 200 | type ChunkedClient struct { | ||
| 202 | - | client *http.Client | 201 | + | client *http.Client |
| 203 | - | baseURL string | 202 | + | baseURL string |
| 204 | - | token string | 203 | + | token string |
| 205 | - | chunkSize int64 | 204 | + | chunkSize int64 |
| 206 | - | app *cli.App | 205 | + | app *cli.App |
| 207 | - | progress *ProgressDisplay | 206 | + | progress *ProgressDisplay |
| 208 | - | cancelFunc context.CancelFunc | 207 | + | cancelFunc context.CancelFunc |
| 209 | - | conflictAction chan common.ConflictAction | |||
| 210 | } | 208 | } | ||
| 211 | 209 | ||||
| 212 | type transferStore struct { | 210 | type transferStore struct { | ||
| @@ -562,11 +560,6 @@ func verifyChunkIntegrity(chunk []byte, metadata common.ChunkMetadata) error { | |||||
| 562 | return nil | 560 | return nil | ||
| 563 | } | 561 | } | ||
| 564 | 562 | ||||
| 565 | - | // isAuthError checks if a response is an authentication error | |||
| 566 | - | func isAuthError(resp *Response) bool { | |||
| 567 | - | return resp != nil && resp.StatusCode == http.StatusUnauthorized | |||
| 568 | - | } | |||
| 569 | - | ||||
| 570 | // cleanupClientTransfer cleans up a client transfer | 563 | // cleanupClientTransfer cleans up a client transfer | ||
| 571 | func cleanupClientTransfer(transferID string) { | 564 | func cleanupClientTransfer(transferID string) { | ||
| 572 | clientChunkStore.mu.Lock() | 565 | clientChunkStore.mu.Lock() | ||
M internal/cli/handler/http.go +1 -1
| @@ -147,7 +147,7 @@ func createRequest(apiUrl, endpoint, method string, rp *common.RequestPayload, t | |||
| 147 | 147 | } | |
| 148 | 148 | ||
| 149 | 149 | // Set the authorization header for all request types | |
| 150 | - | req.Header.Set("Authorization", "Bearer "+token) | |
| 150 | + | setAuthRequestHeaders(req, token) | |
| 151 | 151 | ||
| 152 | 152 | return req, nil | |
| 153 | 153 | } | |
| @@ -147,7 +147,7 @@ func createRequest(apiUrl, endpoint, method string, rp *common.RequestPayload, t | |||||
| 147 | } | 147 | } | ||
| 148 | 148 | ||||
| 149 | // Set the authorization header for all request types | 149 | // Set the authorization header for all request types | ||
| 150 | - | req.Header.Set("Authorization", "Bearer "+token) | 150 | + | setAuthRequestHeaders(req, token) |
| 151 | 151 | ||||
| 152 | return req, nil | 152 | return req, nil | ||
| 153 | } | 153 | } | ||
M internal/common/inputs.go +65 -4
| @@ -1,11 +1,72 @@ | |||
| 1 | 1 | package common | |
| 2 | 2 | ||
| 3 | - | import "fmt" | |
| 3 | + | import ( | |
| 4 | + | "bufio" | |
| 5 | + | "fmt" | |
| 6 | + | "log" | |
| 7 | + | "os" | |
| 8 | + | "strings" | |
| 9 | + | ) | |
| 4 | 10 | ||
| 5 | 11 | // ReadUserInput reads a string input from the user with a prompt. | |
| 12 | + | // It handles multi-word inputs and properly checks for errors. | |
| 6 | 13 | func ReadUserInput(prompt string) string { | |
| 7 | - | fmt.Println(prompt) | |
| 8 | - | var input string | |
| 9 | - | fmt.Scanln(&input) | |
| 14 | + | fmt.Print(prompt + " ") | |
| 15 | + | ||
| 16 | + | // Use bufio.NewReader for better input handling | |
| 17 | + | reader := bufio.NewReader(os.Stdin) | |
| 18 | + | ||
| 19 | + | // ReadString reads until newline, handling spaces in input | |
| 20 | + | input, err := reader.ReadString('\n') | |
| 21 | + | if err != nil { | |
| 22 | + | log.Printf("Error reading input: %v", err) | |
| 23 | + | return "" | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // Trim spaces and newline characters | |
| 27 | + | return strings.TrimSpace(input) | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // ReadUserInputWithValidation reads user input and validates it using a provided function. | |
| 31 | + | // It keeps prompting until valid input is received. | |
| 32 | + | func ReadUserInputWithValidation(prompt string, validate func(string) bool) string { | |
| 33 | + | for { | |
| 34 | + | input := ReadUserInput(prompt) | |
| 35 | + | if validate(input) { | |
| 36 | + | return input | |
| 37 | + | } | |
| 38 | + | fmt.Println("Invalid input. Please try again.") | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // ReadUserInputWithDefault reads user input with a default value if input is empty. | |
| 43 | + | func ReadUserInputWithDefault(prompt string, defaultValue string) string { | |
| 44 | + | fmt.Printf("%s (default: %s) ", prompt, defaultValue) | |
| 45 | + | ||
| 46 | + | reader := bufio.NewReader(os.Stdin) | |
| 47 | + | input, err := reader.ReadString('\n') | |
| 48 | + | if err != nil { | |
| 49 | + | log.Printf("Error reading input: %v", err) | |
| 50 | + | return defaultValue | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | input = strings.TrimSpace(input) | |
| 54 | + | if input == "" { | |
| 55 | + | return defaultValue | |
| 56 | + | } | |
| 10 | 57 | return input | |
| 11 | 58 | } | |
| 59 | + | ||
| 60 | + | // ReadConfirmation reads a yes/no confirmation from the user. | |
| 61 | + | func ReadConfirmation(prompt string) bool { | |
| 62 | + | input := ReadUserInputWithValidation( | |
| 63 | + | prompt+" (y/n)", | |
| 64 | + | func(s string) bool { | |
| 65 | + | s = strings.ToLower(s) | |
| 66 | + | return s == "y" || s == "n" || s == "yes" || s == "no" | |
| 67 | + | }, | |
| 68 | + | ) | |
| 69 | + | ||
| 70 | + | input = strings.ToLower(input) | |
| 71 | + | return input == "y" || input == "yes" | |
| 72 | + | } | |
| @@ -1,11 +1,72 @@ | |||||
| 1 | package common | 1 | package common | ||
| 2 | 2 | ||||
| 3 | - | import "fmt" | 3 | + | import ( |
| 4 | + | "bufio" | |||
| 5 | + | "fmt" | |||
| 6 | + | "log" | |||
| 7 | + | "os" | |||
| 8 | + | "strings" | |||
| 9 | + | ) | |||
| 4 | 10 | ||||
| 5 | // ReadUserInput reads a string input from the user with a prompt. | 11 | // ReadUserInput reads a string input from the user with a prompt. | ||
| 12 | + | // It handles multi-word inputs and properly checks for errors. | |||
| 6 | func ReadUserInput(prompt string) string { | 13 | func ReadUserInput(prompt string) string { | ||
| 7 | - | fmt.Println(prompt) | 14 | + | fmt.Print(prompt + " ") |
| 8 | - | var input string | 15 | + | |
| 9 | - | fmt.Scanln(&input) | 16 | + | // Use bufio.NewReader for better input handling |
| 17 | + | reader := bufio.NewReader(os.Stdin) | |||
| 18 | + | ||||
| 19 | + | // ReadString reads until newline, handling spaces in input | |||
| 20 | + | input, err := reader.ReadString('\n') | |||
| 21 | + | if err != nil { | |||
| 22 | + | log.Printf("Error reading input: %v", err) | |||
| 23 | + | return "" | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // Trim spaces and newline characters | |||
| 27 | + | return strings.TrimSpace(input) | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // ReadUserInputWithValidation reads user input and validates it using a provided function. | |||
| 31 | + | // It keeps prompting until valid input is received. | |||
| 32 | + | func ReadUserInputWithValidation(prompt string, validate func(string) bool) string { | |||
| 33 | + | for { | |||
| 34 | + | input := ReadUserInput(prompt) | |||
| 35 | + | if validate(input) { | |||
| 36 | + | return input | |||
| 37 | + | } | |||
| 38 | + | fmt.Println("Invalid input. Please try again.") | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | // ReadUserInputWithDefault reads user input with a default value if input is empty. | |||
| 43 | + | func ReadUserInputWithDefault(prompt string, defaultValue string) string { | |||
| 44 | + | fmt.Printf("%s (default: %s) ", prompt, defaultValue) | |||
| 45 | + | ||||
| 46 | + | reader := bufio.NewReader(os.Stdin) | |||
| 47 | + | input, err := reader.ReadString('\n') | |||
| 48 | + | if err != nil { | |||
| 49 | + | log.Printf("Error reading input: %v", err) | |||
| 50 | + | return defaultValue | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | input = strings.TrimSpace(input) | |||
| 54 | + | if input == "" { | |||
| 55 | + | return defaultValue | |||
| 56 | + | } | |||
| 10 | return input | 57 | return input | ||
| 11 | } | 58 | } | ||
| 59 | + | ||||
| 60 | + | // ReadConfirmation reads a yes/no confirmation from the user. | |||
| 61 | + | func ReadConfirmation(prompt string) bool { | |||
| 62 | + | input := ReadUserInputWithValidation( | |||
| 63 | + | prompt+" (y/n)", | |||
| 64 | + | func(s string) bool { | |||
| 65 | + | s = strings.ToLower(s) | |||
| 66 | + | return s == "y" || s == "n" || s == "yes" || s == "no" | |||
| 67 | + | }, | |||
| 68 | + | ) | |||
| 69 | + | ||||
| 70 | + | input = strings.ToLower(input) | |||
| 71 | + | return input == "y" || input == "yes" | |||
| 72 | + | } | |||
M internal/httpserve/handlers/endpoint_deploy.go +1 -1
| @@ -30,7 +30,7 @@ func PostDeploy(c echo.Context, a *server.App) error { | |||
| 30 | 30 | }) | |
| 31 | 31 | } | |
| 32 | 32 | ||
| 33 | - | _, err = saveAndImportDeployImage(c, a, payload) | |
| 33 | + | _, err = saveAndImportImage(c, a, payload) | |
| 34 | 34 | if err != nil { | |
| 35 | 35 | // Debug | |
| 36 | 36 | log.Error("Failed to save and import image", "error", err) | |
| @@ -30,7 +30,7 @@ func PostDeploy(c echo.Context, a *server.App) error { | |||||
| 30 | }) | 30 | }) | ||
| 31 | } | 31 | } | ||
| 32 | 32 | ||||
| 33 | - | _, err = saveAndImportDeployImage(c, a, payload) | 33 | + | _, err = saveAndImportImage(c, a, payload) |
| 34 | if err != nil { | 34 | if err != nil { | ||
| 35 | // Debug | 35 | // Debug | ||
| 36 | log.Error("Failed to save and import image", "error", err) | 36 | log.Error("Failed to save and import image", "error", err) | ||
M internal/httpserve/handlers/endpoint_helpers.go +29 -52
| @@ -5,7 +5,6 @@ import ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "fmt" | |
| 7 | 7 | "net/http" | |
| 8 | - | "os" | |
| 9 | 8 | "regexp" | |
| 10 | 9 | "sync" | |
| 11 | 10 | "time" | |
| @@ -132,66 +131,59 @@ func validateAndPrepareDeployPayload(c echo.Context) (*common.DeployPayload, err | |||
| 132 | 131 | return payload, nil | |
| 133 | 132 | } | |
| 134 | 133 | ||
| 135 | - | // saveAndImportPushImage saves the image to the storage and imports it to the Docker engine, updating the payload with the image ID and returning the path to the saved image | |
| 136 | - | func saveAndImportPushImage(c echo.Context, a *server.App, payload *common.PushPayload) (string, error) { | |
| 137 | - | imageReader := c.Request().Body | |
| 138 | - | defer imageReader.Close() | |
| 139 | - | ||
| 140 | - | imageFileName := sanitizeImageFileName(payload.ImageName) | |
| 141 | - | ||
| 142 | - | imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) | |
| 143 | - | if err != nil { | |
| 144 | - | return "", fmt.Errorf("failed to save image: %v", err) | |
| 145 | - | } | |
| 146 | - | ||
| 147 | - | imageID, err := docker.ImportImageToEngine(imagePath) | |
| 148 | - | if err != nil { | |
| 149 | - | store.RemoveFromStorage(imagePath) | |
| 150 | - | return "", fmt.Errorf("failed to import image: %v", err) | |
| 134 | + | // ssaveAndImportImage saves and imports an image from the request body | |
| 135 | + | func saveAndImportImage(c echo.Context, a *server.App, payload interface{}) (string, error) { | |
| 136 | + | var imageName, imageID string | |
| 137 | + | ||
| 138 | + | // Determine payload type and extract image name | |
| 139 | + | switch p := payload.(type) { | |
| 140 | + | case *common.PushPayload: | |
| 141 | + | imageName = p.ImageName | |
| 142 | + | defer func() { p.ImageID = imageID }() | |
| 143 | + | case *common.DeployPayload: | |
| 144 | + | imageName = p.ImageName | |
| 145 | + | defer func() { p.ImageID = imageID }() | |
| 146 | + | default: | |
| 147 | + | return "", fmt.Errorf("unsupported payload type: %T", payload) | |
| 151 | 148 | } | |
| 152 | 149 | ||
| 153 | - | if imageID == "" { | |
| 154 | - | return "", errors.New("imported image ID is empty") | |
| 155 | - | } | |
| 156 | - | ||
| 157 | - | err = store.RemoveFromStorage(imagePath) | |
| 158 | - | if err != nil { | |
| 159 | - | return "", fmt.Errorf("failed to remove temporary image file: %v", err) | |
| 160 | - | } | |
| 161 | - | ||
| 162 | - | payload.ImageID = imageID | |
| 163 | - | return imagePath, nil | |
| 164 | - | } | |
| 165 | - | ||
| 166 | - | // saveAndImportDeployImage saves the image to the storage and imports it to the Docker engine, updating the payload with the image ID and returning the path to the saved image | |
| 167 | - | func saveAndImportDeployImage(c echo.Context, a *server.App, payload *common.DeployPayload) (string, error) { | |
| 168 | - | ||
| 169 | 150 | imageReader := c.Request().Body | |
| 170 | 151 | defer imageReader.Close() | |
| 171 | 152 | ||
| 172 | - | imageFileName := sanitizeImageFileName(payload.ImageName) | |
| 153 | + | imageFileName := sanitizeImageFileName(imageName) | |
| 173 | 154 | ||
| 174 | 155 | imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) | |
| 175 | 156 | if err != nil { | |
| 176 | 157 | return "", fmt.Errorf("failed to save image: %v", err) | |
| 177 | 158 | } | |
| 178 | 159 | ||
| 179 | - | imageID, err := docker.ImportImageToEngine(imagePath) | |
| 160 | + | imageID, err = docker.ImportImageToEngine(imagePath) | |
| 180 | 161 | if err != nil { | |
| 181 | - | store.RemoveFromStorage(imagePath) | |
| 162 | + | if removeErr := store.RemoveFromStorage(imagePath); removeErr != nil { | |
| 163 | + | log.Error("Failed to remove temporary image file after import failure", | |
| 164 | + | "error", removeErr, | |
| 165 | + | "path", imagePath) | |
| 166 | + | } | |
| 182 | 167 | return "", fmt.Errorf("failed to import image: %v", err) | |
| 183 | 168 | } | |
| 184 | 169 | ||
| 185 | 170 | if imageID == "" { | |
| 171 | + | if removeErr := store.RemoveFromStorage(imagePath); removeErr != nil { | |
| 172 | + | log.Error("Failed to remove temporary image file after empty ID", | |
| 173 | + | "error", removeErr, | |
| 174 | + | "path", imagePath) | |
| 175 | + | } | |
| 186 | 176 | return "", errors.New("imported image ID is empty") | |
| 187 | 177 | } | |
| 188 | 178 | ||
| 189 | 179 | err = store.RemoveFromStorage(imagePath) | |
| 190 | 180 | if err != nil { | |
| 181 | + | log.Error("Failed to remove temporary image file after successful import", | |
| 182 | + | "error", err, | |
| 183 | + | "path", imagePath) | |
| 191 | 184 | return "", fmt.Errorf("failed to remove temporary image file: %v", err) | |
| 192 | 185 | } | |
| 193 | 186 | ||
| 194 | - | payload.ImageID = imageID | |
| 195 | 187 | return imagePath, nil | |
| 196 | 188 | } | |
| 197 | 189 | ||
| @@ -357,21 +349,6 @@ func extractContainerID(errorMessage string) string { | |||
| 357 | 349 | return "" | |
| 358 | 350 | } | |
| 359 | 351 | ||
| 360 | - | // verifyFileContents checks if the file size matches the expected size | |
| 361 | - | func verifyFileContents(file *os.File, expectedSize int64) error { | |
| 362 | - | actualSize, err := file.Seek(0, 2) // Seek to end | |
| 363 | - | if err != nil { | |
| 364 | - | return fmt.Errorf("failed to verify file size: %w", err) | |
| 365 | - | } | |
| 366 | - | ||
| 367 | - | if actualSize != expectedSize { | |
| 368 | - | return fmt.Errorf("final file size mismatch: got %d, expected %d", actualSize, expectedSize) | |
| 369 | - | } | |
| 370 | - | ||
| 371 | - | _, err = file.Seek(0, 0) // Reset to beginning | |
| 372 | - | return err | |
| 373 | - | } | |
| 374 | - | ||
| 375 | 352 | // storeIDMapping stores a mapping between a short ID and a full ID | |
| 376 | 353 | func storeIDMapping(shortID, fullID string) { | |
| 377 | 354 | safelyInteractWithIDMap(Update, shortID, fullID) | |
| @@ -5,7 +5,6 @@ import ( | |||||
| 5 | "errors" | 5 | "errors" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "net/http" | 7 | "net/http" | ||
| 8 | - | "os" | |||
| 9 | "regexp" | 8 | "regexp" | ||
| 10 | "sync" | 9 | "sync" | ||
| 11 | "time" | 10 | "time" | ||
| @@ -132,66 +131,59 @@ func validateAndPrepareDeployPayload(c echo.Context) (*common.DeployPayload, err | |||||
| 132 | return payload, nil | 131 | return payload, nil | ||
| 133 | } | 132 | } | ||
| 134 | 133 | ||||
| 135 | - | // saveAndImportPushImage saves the image to the storage and imports it to the Docker engine, updating the payload with the image ID and returning the path to the saved image | 134 | + | // ssaveAndImportImage saves and imports an image from the request body |
| 136 | - | func saveAndImportPushImage(c echo.Context, a *server.App, payload *common.PushPayload) (string, error) { | 135 | + | func saveAndImportImage(c echo.Context, a *server.App, payload interface{}) (string, error) { |
| 137 | - | imageReader := c.Request().Body | 136 | + | var imageName, imageID string |
| 138 | - | defer imageReader.Close() | 137 | + | |
| 139 | - | 138 | + | // Determine payload type and extract image name | |
| 140 | - | imageFileName := sanitizeImageFileName(payload.ImageName) | 139 | + | switch p := payload.(type) { |
| 141 | - | 140 | + | case *common.PushPayload: | |
| 142 | - | imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) | 141 | + | imageName = p.ImageName |
| 143 | - | if err != nil { | 142 | + | defer func() { p.ImageID = imageID }() |
| 144 | - | return "", fmt.Errorf("failed to save image: %v", err) | 143 | + | case *common.DeployPayload: |
| 145 | - | } | 144 | + | imageName = p.ImageName |
| 146 | - | 145 | + | defer func() { p.ImageID = imageID }() | |
| 147 | - | imageID, err := docker.ImportImageToEngine(imagePath) | 146 | + | default: |
| 148 | - | if err != nil { | 147 | + | return "", fmt.Errorf("unsupported payload type: %T", payload) |
| 149 | - | store.RemoveFromStorage(imagePath) | |||
| 150 | - | return "", fmt.Errorf("failed to import image: %v", err) | |||
| 151 | } | 148 | } | ||
| 152 | 149 | ||||
| 153 | - | if imageID == "" { | |||
| 154 | - | return "", errors.New("imported image ID is empty") | |||
| 155 | - | } | |||
| 156 | - | ||||
| 157 | - | err = store.RemoveFromStorage(imagePath) | |||
| 158 | - | if err != nil { | |||
| 159 | - | return "", fmt.Errorf("failed to remove temporary image file: %v", err) | |||
| 160 | - | } | |||
| 161 | - | ||||
| 162 | - | payload.ImageID = imageID | |||
| 163 | - | return imagePath, nil | |||
| 164 | - | } | |||
| 165 | - | ||||
| 166 | - | // saveAndImportDeployImage saves the image to the storage and imports it to the Docker engine, updating the payload with the image ID and returning the path to the saved image | |||
| 167 | - | func saveAndImportDeployImage(c echo.Context, a *server.App, payload *common.DeployPayload) (string, error) { | |||
| 168 | - | ||||
| 169 | imageReader := c.Request().Body | 150 | imageReader := c.Request().Body | ||
| 170 | defer imageReader.Close() | 151 | defer imageReader.Close() | ||
| 171 | 152 | ||||
| 172 | - | imageFileName := sanitizeImageFileName(payload.ImageName) | 153 | + | imageFileName := sanitizeImageFileName(imageName) |
| 173 | 154 | ||||
| 174 | imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) | 155 | imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) | ||
| 175 | if err != nil { | 156 | if err != nil { | ||
| 176 | return "", fmt.Errorf("failed to save image: %v", err) | 157 | return "", fmt.Errorf("failed to save image: %v", err) | ||
| 177 | } | 158 | } | ||
| 178 | 159 | ||||
| 179 | - | imageID, err := docker.ImportImageToEngine(imagePath) | 160 | + | imageID, err = docker.ImportImageToEngine(imagePath) |
| 180 | if err != nil { | 161 | if err != nil { | ||
| 181 | - | store.RemoveFromStorage(imagePath) | 162 | + | if removeErr := store.RemoveFromStorage(imagePath); removeErr != nil { |
| 163 | + | log.Error("Failed to remove temporary image file after import failure", | |||
| 164 | + | "error", removeErr, | |||
| 165 | + | "path", imagePath) | |||
| 166 | + | } | |||
| 182 | return "", fmt.Errorf("failed to import image: %v", err) | 167 | return "", fmt.Errorf("failed to import image: %v", err) | ||
| 183 | } | 168 | } | ||
| 184 | 169 | ||||
| 185 | if imageID == "" { | 170 | if imageID == "" { | ||
| 171 | + | if removeErr := store.RemoveFromStorage(imagePath); removeErr != nil { | |||
| 172 | + | log.Error("Failed to remove temporary image file after empty ID", | |||
| 173 | + | "error", removeErr, | |||
| 174 | + | "path", imagePath) | |||
| 175 | + | } | |||
| 186 | return "", errors.New("imported image ID is empty") | 176 | return "", errors.New("imported image ID is empty") | ||
| 187 | } | 177 | } | ||
| 188 | 178 | ||||
| 189 | err = store.RemoveFromStorage(imagePath) | 179 | err = store.RemoveFromStorage(imagePath) | ||
| 190 | if err != nil { | 180 | if err != nil { | ||
| 181 | + | log.Error("Failed to remove temporary image file after successful import", | |||
| 182 | + | "error", err, | |||
| 183 | + | "path", imagePath) | |||
| 191 | return "", fmt.Errorf("failed to remove temporary image file: %v", err) | 184 | return "", fmt.Errorf("failed to remove temporary image file: %v", err) | ||
| 192 | } | 185 | } | ||
| 193 | 186 | ||||
| 194 | - | payload.ImageID = imageID | |||
| 195 | return imagePath, nil | 187 | return imagePath, nil | ||
| 196 | } | 188 | } | ||
| 197 | 189 | ||||
| @@ -357,21 +349,6 @@ func extractContainerID(errorMessage string) string { | |||||
| 357 | return "" | 349 | return "" | ||
| 358 | } | 350 | } | ||
| 359 | 351 | ||||
| 360 | - | // verifyFileContents checks if the file size matches the expected size | |||
| 361 | - | func verifyFileContents(file *os.File, expectedSize int64) error { | |||
| 362 | - | actualSize, err := file.Seek(0, 2) // Seek to end | |||
| 363 | - | if err != nil { | |||
| 364 | - | return fmt.Errorf("failed to verify file size: %w", err) | |||
| 365 | - | } | |||
| 366 | - | ||||
| 367 | - | if actualSize != expectedSize { | |||
| 368 | - | return fmt.Errorf("final file size mismatch: got %d, expected %d", actualSize, expectedSize) | |||
| 369 | - | } | |||
| 370 | - | ||||
| 371 | - | _, err = file.Seek(0, 0) // Reset to beginning | |||
| 372 | - | return err | |||
| 373 | - | } | |||
| 374 | - | ||||
| 375 | // storeIDMapping stores a mapping between a short ID and a full ID | 352 | // storeIDMapping stores a mapping between a short ID and a full ID | ||
| 376 | func storeIDMapping(shortID, fullID string) { | 353 | func storeIDMapping(shortID, fullID string) { | ||
| 377 | safelyInteractWithIDMap(Update, shortID, fullID) | 354 | safelyInteractWithIDMap(Update, shortID, fullID) | ||
M internal/httpserve/handlers/endpoint_push.go +1 -45
| @@ -2,7 +2,6 @@ | |||
| 2 | 2 | package handlers | |
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | - | "archive/tar" | |
| 6 | 5 | "bytes" | |
| 7 | 6 | "encoding/json" | |
| 8 | 7 | "fmt" | |
| @@ -35,7 +34,7 @@ func PostPush(c echo.Context, a *server.App) error { | |||
| 35 | 34 | }) | |
| 36 | 35 | } | |
| 37 | 36 | ||
| 38 | - | _, err = saveAndImportPushImage(c, a, payload) | |
| 37 | + | _, err = saveAndImportImage(c, a, payload) | |
| 39 | 38 | if err != nil { | |
| 40 | 39 | return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ | |
| 41 | 40 | Success: false, | |
| @@ -176,26 +175,6 @@ func getTransferData(transferID string) (*ChunkMetadata, map[int][]byte, error) | |||
| 176 | 175 | return metadata, chunkStore.chunks[transferID], nil | |
| 177 | 176 | } | |
| 178 | 177 | ||
| 179 | - | func createTempFile() (*os.File, func(), error) { | |
| 180 | - | tmpDir, err := os.MkdirTemp("", "docker-import-*") | |
| 181 | - | if err != nil { | |
| 182 | - | return nil, nil, err | |
| 183 | - | } | |
| 184 | - | ||
| 185 | - | tmpFile, err := os.Create(filepath.Join(tmpDir, "image.tar")) | |
| 186 | - | if err != nil { | |
| 187 | - | os.RemoveAll(tmpDir) | |
| 188 | - | return nil, nil, err | |
| 189 | - | } | |
| 190 | - | ||
| 191 | - | cleanup := func() { | |
| 192 | - | tmpFile.Close() | |
| 193 | - | os.RemoveAll(tmpDir) | |
| 194 | - | } | |
| 195 | - | ||
| 196 | - | return tmpFile, cleanup, nil | |
| 197 | - | } | |
| 198 | - | ||
| 199 | 178 | func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) error { | |
| 200 | 179 | // First, seek to the beginning of the file | |
| 201 | 180 | if _, err := file.Seek(0, 0); err != nil { | |
| @@ -235,29 +214,6 @@ func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) er | |||
| 235 | 214 | return nil | |
| 236 | 215 | } | |
| 237 | 216 | ||
| 238 | - | func prepareFileForImport(file *os.File) error { | |
| 239 | - | // Reset to beginning of file | |
| 240 | - | if _, err := file.Seek(0, 0); err != nil { | |
| 241 | - | return fmt.Errorf("failed to seek to beginning: %w", err) | |
| 242 | - | } | |
| 243 | - | ||
| 244 | - | // Verify the tar format | |
| 245 | - | tr := tar.NewReader(file) | |
| 246 | - | header, err := tr.Next() | |
| 247 | - | if err != nil { | |
| 248 | - | return fmt.Errorf("invalid tar format: %w", err) | |
| 249 | - | } | |
| 250 | - | ||
| 251 | - | // Check if this is a valid Docker image tar | |
| 252 | - | if header.Name == "manifest.json" { | |
| 253 | - | // Reset file pointer for subsequent reads | |
| 254 | - | _, err = file.Seek(0, 0) | |
| 255 | - | return err | |
| 256 | - | } | |
| 257 | - | ||
| 258 | - | return fmt.Errorf("file does not appear to be a valid Docker image archive") | |
| 259 | - | } | |
| 260 | - | ||
| 261 | 217 | func sendErrorResponse(c echo.Context, message string, err error) error { | |
| 262 | 218 | log.Error(message, "error", err) | |
| 263 | 219 | return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ | |
| @@ -2,7 +2,6 @@ | |||||
| 2 | package handlers | 2 | package handlers | ||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | - | "archive/tar" | |||
| 6 | "bytes" | 5 | "bytes" | ||
| 7 | "encoding/json" | 6 | "encoding/json" | ||
| 8 | "fmt" | 7 | "fmt" | ||
| @@ -35,7 +34,7 @@ func PostPush(c echo.Context, a *server.App) error { | |||||
| 35 | }) | 34 | }) | ||
| 36 | } | 35 | } | ||
| 37 | 36 | ||||
| 38 | - | _, err = saveAndImportPushImage(c, a, payload) | 37 | + | _, err = saveAndImportImage(c, a, payload) |
| 39 | if err != nil { | 38 | if err != nil { | ||
| 40 | return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ | 39 | return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ | ||
| 41 | Success: false, | 40 | Success: false, | ||
| @@ -176,26 +175,6 @@ func getTransferData(transferID string) (*ChunkMetadata, map[int][]byte, error) | |||||
| 176 | return metadata, chunkStore.chunks[transferID], nil | 175 | return metadata, chunkStore.chunks[transferID], nil | ||
| 177 | } | 176 | } | ||
| 178 | 177 | ||||
| 179 | - | func createTempFile() (*os.File, func(), error) { | |||
| 180 | - | tmpDir, err := os.MkdirTemp("", "docker-import-*") | |||
| 181 | - | if err != nil { | |||
| 182 | - | return nil, nil, err | |||
| 183 | - | } | |||
| 184 | - | ||||
| 185 | - | tmpFile, err := os.Create(filepath.Join(tmpDir, "image.tar")) | |||
| 186 | - | if err != nil { | |||
| 187 | - | os.RemoveAll(tmpDir) | |||
| 188 | - | return nil, nil, err | |||
| 189 | - | } | |||
| 190 | - | ||||
| 191 | - | cleanup := func() { | |||
| 192 | - | tmpFile.Close() | |||
| 193 | - | os.RemoveAll(tmpDir) | |||
| 194 | - | } | |||
| 195 | - | ||||
| 196 | - | return tmpFile, cleanup, nil | |||
| 197 | - | } | |||
| 198 | - | ||||
| 199 | func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) error { | 178 | func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) error { | ||
| 200 | // First, seek to the beginning of the file | 179 | // First, seek to the beginning of the file | ||
| 201 | if _, err := file.Seek(0, 0); err != nil { | 180 | if _, err := file.Seek(0, 0); err != nil { | ||
| @@ -235,29 +214,6 @@ func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) er | |||||
| 235 | return nil | 214 | return nil | ||
| 236 | } | 215 | } | ||
| 237 | 216 | ||||
| 238 | - | func prepareFileForImport(file *os.File) error { | |||
| 239 | - | // Reset to beginning of file | |||
| 240 | - | if _, err := file.Seek(0, 0); err != nil { | |||
| 241 | - | return fmt.Errorf("failed to seek to beginning: %w", err) | |||
| 242 | - | } | |||
| 243 | - | ||||
| 244 | - | // Verify the tar format | |||
| 245 | - | tr := tar.NewReader(file) | |||
| 246 | - | header, err := tr.Next() | |||
| 247 | - | if err != nil { | |||
| 248 | - | return fmt.Errorf("invalid tar format: %w", err) | |||
| 249 | - | } | |||
| 250 | - | ||||
| 251 | - | // Check if this is a valid Docker image tar | |||
| 252 | - | if header.Name == "manifest.json" { | |||
| 253 | - | // Reset file pointer for subsequent reads | |||
| 254 | - | _, err = file.Seek(0, 0) | |||
| 255 | - | return err | |||
| 256 | - | } | |||
| 257 | - | ||||
| 258 | - | return fmt.Errorf("file does not appear to be a valid Docker image archive") | |||
| 259 | - | } | |||
| 260 | - | ||||
| 261 | func sendErrorResponse(c echo.Context, message string, err error) error { | 217 | func sendErrorResponse(c echo.Context, message string, err error) error { | ||
| 262 | log.Error(message, "error", err) | 218 | log.Error(message, "error", err) | ||
| 263 | return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ | 219 | return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ | ||
M internal/httpserve/handlers/endpoint_remove.go +30 -30
| @@ -11,34 +11,34 @@ import ( | |||
| 11 | 11 | ||
| 12 | 12 | // PostContainerRemove handles the container remove request | |
| 13 | 13 | func PostContainerRemove(c echo.Context, a *server.App) error { | |
| 14 | - | payload := new(common.RequestPayload) | |
| 15 | - | if err := c.Bind(payload); err != nil { | |
| 16 | - | return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error()) | |
| 17 | - | } | |
| 18 | - | ||
| 19 | - | if payload.Type != "remove" { | |
| 20 | - | return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type") | |
| 21 | - | } | |
| 22 | - | ||
| 23 | - | removePayload, ok := payload.Payload.(common.RemovePayload) | |
| 24 | - | if !ok { | |
| 25 | - | return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure") | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | if removePayload.ContainerID == "" { | |
| 29 | - | return sendJSONResponse(c, http.StatusBadRequest, "Invalid container ID") | |
| 30 | - | } | |
| 31 | - | ||
| 32 | - | err := docker.RemoveContainer(removePayload.ContainerID) | |
| 33 | - | if err != nil { | |
| 34 | - | return sendJSONResponse(c, http.StatusInternalServerError, RemoveResponse{ | |
| 35 | - | Success: false, | |
| 36 | - | Message: err.Error(), | |
| 37 | - | }) | |
| 38 | - | } | |
| 39 | - | ||
| 40 | - | return sendJSONResponse(c, http.StatusOK, RemoveResponse{ | |
| 41 | - | Success: true, | |
| 42 | - | Message: "Container removed successfully", | |
| 43 | - | }) | |
| 14 | + | payload := new(common.RequestPayload) | |
| 15 | + | if err := c.Bind(payload); err != nil { | |
| 16 | + | return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error()) | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | if payload.Type != "remove" { | |
| 20 | + | return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type") | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | removePayload, ok := payload.Payload.(common.RemovePayload) | |
| 24 | + | if !ok { | |
| 25 | + | return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure") | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | if removePayload.ContainerID == "" { | |
| 29 | + | return sendJSONResponse(c, http.StatusBadRequest, "Invalid container ID") | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | err := docker.RemoveContainer(removePayload.ContainerID) | |
| 33 | + | if err != nil { | |
| 34 | + | return sendJSONResponse(c, http.StatusInternalServerError, RemoveResponse{ | |
| 35 | + | Success: false, | |
| 36 | + | Message: err.Error(), | |
| 37 | + | }) | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | return sendJSONResponse(c, http.StatusOK, RemoveResponse{ | |
| 41 | + | Success: true, | |
| 42 | + | Message: "Container removed successfully", | |
| 43 | + | }) | |
| 44 | 44 | } | |
| @@ -11,34 +11,34 @@ import ( | |||||
| 11 | 11 | ||||
| 12 | // PostContainerRemove handles the container remove request | 12 | // PostContainerRemove handles the container remove request | ||
| 13 | func PostContainerRemove(c echo.Context, a *server.App) error { | 13 | func PostContainerRemove(c echo.Context, a *server.App) error { | ||
| 14 | - | payload := new(common.RequestPayload) | 14 | + | payload := new(common.RequestPayload) |
| 15 | - | if err := c.Bind(payload); err != nil { | 15 | + | if err := c.Bind(payload); err != nil { |
| 16 | - | return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error()) | 16 | + | return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload: "+err.Error()) |
| 17 | - | } | 17 | + | } |
| 18 | - | 18 | + | ||
| 19 | - | if payload.Type != "remove" { | 19 | + | if payload.Type != "remove" { |
| 20 | - | return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type") | 20 | + | return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload type") |
| 21 | - | } | 21 | + | } |
| 22 | - | 22 | + | ||
| 23 | - | removePayload, ok := payload.Payload.(common.RemovePayload) | 23 | + | removePayload, ok := payload.Payload.(common.RemovePayload) |
| 24 | - | if !ok { | 24 | + | if !ok { |
| 25 | - | return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure") | 25 | + | return sendJSONResponse(c, http.StatusBadRequest, "Invalid payload structure") |
| 26 | - | } | 26 | + | } |
| 27 | - | 27 | + | ||
| 28 | - | if removePayload.ContainerID == "" { | 28 | + | if removePayload.ContainerID == "" { |
| 29 | - | return sendJSONResponse(c, http.StatusBadRequest, "Invalid container ID") | 29 | + | return sendJSONResponse(c, http.StatusBadRequest, "Invalid container ID") |
| 30 | - | } | 30 | + | } |
| 31 | - | 31 | + | ||
| 32 | - | err := docker.RemoveContainer(removePayload.ContainerID) | 32 | + | err := docker.RemoveContainer(removePayload.ContainerID) |
| 33 | - | if err != nil { | 33 | + | if err != nil { |
| 34 | - | return sendJSONResponse(c, http.StatusInternalServerError, RemoveResponse{ | 34 | + | return sendJSONResponse(c, http.StatusInternalServerError, RemoveResponse{ |
| 35 | - | Success: false, | 35 | + | Success: false, |
| 36 | - | Message: err.Error(), | 36 | + | Message: err.Error(), |
| 37 | - | }) | 37 | + | }) |
| 38 | - | } | 38 | + | } |
| 39 | - | 39 | + | ||
| 40 | - | return sendJSONResponse(c, http.StatusOK, RemoveResponse{ | 40 | + | return sendJSONResponse(c, http.StatusOK, RemoveResponse{ |
| 41 | - | Success: true, | 41 | + | Success: true, |
| 42 | - | Message: "Container removed successfully", | 42 | + | Message: "Container removed successfully", |
| 43 | - | }) | 43 | + | }) |
| 44 | } | 44 | } | ||
M internal/server/sqlite_close.go +19 -12
| @@ -1,23 +1,30 @@ | |||
| 1 | 1 | package server | |
| 2 | 2 | ||
| 3 | + | import "fmt" | |
| 4 | + | ||
| 3 | 5 | // CloseAndSaveDB closes the in-memory database and backup the database file if there is any modification. | |
| 4 | 6 | func CloseDB(a *App) error { | |
| 5 | 7 | if a.DB != nil { | |
| 6 | - | saveDB(a) | |
| 7 | - | a.DB.Close() | |
| 8 | + | // if err := saveDB(a); err != nil { | |
| 9 | + | // return fmt.Errorf("failed to save database: %w", err) | |
| 10 | + | // } | |
| 11 | + | ||
| 12 | + | if err := a.DB.Close(); err != nil { | |
| 13 | + | return fmt.Errorf("failed to close database: %w", err) | |
| 14 | + | } | |
| 8 | 15 | } | |
| 9 | 16 | return nil | |
| 10 | 17 | } | |
| 11 | 18 | ||
| 12 | - | func saveDB(a *App) error { | |
| 19 | + | // func saveDB(a *App) error { | |
| 13 | 20 | ||
| 14 | - | return nil | |
| 15 | - | } | |
| 21 | + | // return nil | |
| 22 | + | // } | |
| 16 | 23 | ||
| 17 | - | // TODO | |
| 18 | - | func backupDBFile(a *App) error { | |
| 19 | - | // Create a backup of the database file at the same location as the database file. | |
| 20 | - | // The backup file name is the database file name with a timestamp appended to it. | |
| 21 | - | // If the backup file already exists, overwrite it. | |
| 22 | - | return nil | |
| 23 | - | } | |
| 24 | + | // // TODO | |
| 25 | + | // func backupDBFile(a *App) error { | |
| 26 | + | // // Create a backup of the database file at the same location as the database file. | |
| 27 | + | // // The backup file name is the database file name with a timestamp appended to it. | |
| 28 | + | // // If the backup file already exists, overwrite it. | |
| 29 | + | // return nil | |
| 30 | + | // } | |
| @@ -1,23 +1,30 @@ | |||||
| 1 | package server | 1 | package server | ||
| 2 | 2 | ||||
| 3 | + | import "fmt" | |||
| 4 | + | ||||
| 3 | // CloseAndSaveDB closes the in-memory database and backup the database file if there is any modification. | 5 | // CloseAndSaveDB closes the in-memory database and backup the database file if there is any modification. | ||
| 4 | func CloseDB(a *App) error { | 6 | func CloseDB(a *App) error { | ||
| 5 | if a.DB != nil { | 7 | if a.DB != nil { | ||
| 6 | - | saveDB(a) | 8 | + | // if err := saveDB(a); err != nil { |
| 7 | - | a.DB.Close() | 9 | + | // return fmt.Errorf("failed to save database: %w", err) |
| 10 | + | // } | |||
| 11 | + | ||||
| 12 | + | if err := a.DB.Close(); err != nil { | |||
| 13 | + | return fmt.Errorf("failed to close database: %w", err) | |||
| 14 | + | } | |||
| 8 | } | 15 | } | ||
| 9 | return nil | 16 | return nil | ||
| 10 | } | 17 | } | ||
| 11 | 18 | ||||
| 12 | - | func saveDB(a *App) error { | 19 | + | // func saveDB(a *App) error { |
| 13 | 20 | ||||
| 14 | - | return nil | 21 | + | // return nil |
| 15 | - | } | 22 | + | // } |
| 16 | 23 | ||||
| 17 | - | // TODO | 24 | + | // // TODO |
| 18 | - | func backupDBFile(a *App) error { | 25 | + | // func backupDBFile(a *App) error { |
| 19 | - | // Create a backup of the database file at the same location as the database file. | 26 | + | // // Create a backup of the database file at the same location as the database file. |
| 20 | - | // The backup file name is the database file name with a timestamp appended to it. | 27 | + | // // The backup file name is the database file name with a timestamp appended to it. |
| 21 | - | // If the backup file already exists, overwrite it. | 28 | + | // // If the backup file already exists, overwrite it. |
| 22 | - | return nil | 29 | + | // return nil |
| 23 | - | } | 30 | + | // } |
M internal/templating/cmdparams/from_inputs.go +0 -7
| @@ -45,10 +45,3 @@ func FromInputsToCmdParams(inputs map[string]string, a *server.App) (docker.Cont | |||
| 45 | 45 | ||
| 46 | 46 | return params, nil | |
| 47 | 47 | } | |
| 48 | - | ||
| 49 | - | func extractContainerPort(portMappings []docker.PortMapping) string { | |
| 50 | - | if len(portMappings) > 0 { | |
| 51 | - | return portMappings[0].ContainerPort | |
| 52 | - | } | |
| 53 | - | return "" | |
| 54 | - | } | |
| @@ -45,10 +45,3 @@ func FromInputsToCmdParams(inputs map[string]string, a *server.App) (docker.Cont | |||||
| 45 | 45 | ||||
| 46 | return params, nil | 46 | return params, nil | ||
| 47 | } | 47 | } | ||
| 48 | - | ||||
| 49 | - | func extractContainerPort(portMappings []docker.PortMapping) string { | |||
| 50 | - | if len(portMappings) > 0 { | |||
| 51 | - | return portMappings[0].ContainerPort | |||
| 52 | - | } | |||
| 53 | - | return "" | |||
| 54 | - | } | |||
M pkg/docker/containers.go +125 -60
| @@ -109,8 +109,22 @@ func ListRunningContainers() ([]types.Container, error) { | |||
| 109 | 109 | ||
| 110 | 110 | // StopContainer try to stop a container gracefully, if it fails, it will stop it forcefully | |
| 111 | 111 | func StopContainer(containerID string) error { | |
| 112 | - | StopContainerGracefully(containerID, 3*time.Second) | |
| 113 | - | StopContainerRagefully(containerID) | |
| 112 | + | // Try graceful shutdown first | |
| 113 | + | stopped, err := StopContainerGracefully(containerID, 3*time.Second) | |
| 114 | + | if err != nil { | |
| 115 | + | log.Warn("Failed to stop container gracefully", "error", err) | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // If container wasn't stopped gracefully or there was an error, try forceful shutdown | |
| 119 | + | if !stopped { | |
| 120 | + | if err := StopContainerRagefully(containerID); err != nil { | |
| 121 | + | return fmt.Errorf("failed to stop container forcefully: %w", err) | |
| 122 | + | } | |
| 123 | + | log.Info("Container stopped forcefully", "containerID", containerID) | |
| 124 | + | } else { | |
| 125 | + | log.Info("Container stopped gracefully", "containerID", containerID) | |
| 126 | + | } | |
| 127 | + | ||
| 114 | 128 | return nil | |
| 115 | 129 | } | |
| 116 | 130 | ||
| @@ -127,18 +141,16 @@ func StopContainerGracefully(containerID string, timeoutDuration time.Duration) | |||
| 127 | 141 | defer ticker.Stop() | |
| 128 | 142 | ||
| 129 | 143 | for elapsed := 0; elapsed < int(timeoutDuration.Seconds()); elapsed++ { | |
| 130 | - | select { | |
| 131 | - | case <-ticker.C: | |
| 132 | - | // Check if the container is still running | |
| 133 | - | container, err := dockerCli.ContainerInspect(context.Background(), containerID) | |
| 134 | - | if err != nil { | |
| 135 | - | return false, err | |
| 136 | - | } | |
| 144 | + | <-ticker.C | |
| 145 | + | // Check if the container is still running | |
| 146 | + | container, err := dockerCli.ContainerInspect(context.Background(), containerID) | |
| 147 | + | if err != nil { | |
| 148 | + | return false, err | |
| 149 | + | } | |
| 137 | 150 | ||
| 138 | - | // If the container is not running, return true indicating it was stopped | |
| 139 | - | if !container.State.Running { | |
| 140 | - | return true, nil | |
| 141 | - | } | |
| 151 | + | // If the container is not running, return true | |
| 152 | + | if !container.State.Running { | |
| 153 | + | return true, nil | |
| 142 | 154 | } | |
| 143 | 155 | } | |
| 144 | 156 | ||
| @@ -229,7 +241,10 @@ func StartContainer(containerID string) error { | |||
| 229 | 241 | ||
| 230 | 242 | // CreateContainer creates a container with the given parameters | |
| 231 | 243 | func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | |
| 232 | - | CheckIfInitialized() | |
| 244 | + | ||
| 245 | + | if err := CheckIfInitialized(); err != nil { | |
| 246 | + | return "", fmt.Errorf("failed to check if docker client is initialized: %w", err) | |
| 247 | + | } | |
| 233 | 248 | ||
| 234 | 249 | log.Info("Creating container with params: %+v\n", cmdParams) | |
| 235 | 250 | ||
| @@ -241,7 +256,7 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | |||
| 241 | 256 | ||
| 242 | 257 | if !isNetworkCreated { | |
| 243 | 258 | fmt.Printf("Creating network: %s\n", cmdParams.Network) | |
| 244 | - | _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, types.NetworkCreate{}) | |
| 259 | + | _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, network.CreateOptions{}) | |
| 245 | 260 | if err != nil { | |
| 246 | 261 | return "", fmt.Errorf("network creation failed: %v", err) | |
| 247 | 262 | } | |
| @@ -334,7 +349,7 @@ func WaitForContainerToBeRunning(containerID string, timeout time.Duration) erro | |||
| 334 | 349 | } | |
| 335 | 350 | ||
| 336 | 351 | func CheckIfNetworkExists(networkName string) (bool, error) { | |
| 337 | - | networks, err := dockerCli.NetworkList(context.Background(), types.NetworkListOptions{}) | |
| 352 | + | networks, err := dockerCli.NetworkList(context.Background(), network.ListOptions{}) | |
| 338 | 353 | if err != nil { | |
| 339 | 354 | return false, err | |
| 340 | 355 | } | |
| @@ -349,14 +364,13 @@ func CheckIfNetworkExists(networkName string) (bool, error) { | |||
| 349 | 364 | } | |
| 350 | 365 | ||
| 351 | 366 | // GetNetworkInfo returns information about a network | |
| 352 | - | func GetNetworkInfo(networkName string) (types.NetworkResource, error) { | |
| 353 | - | // Get network info using the Docker client | |
| 354 | - | networkInfo, err := dockerCli.NetworkInspect(context.Background(), networkName, types.NetworkInspectOptions{}) | |
| 367 | + | func GetNetworkInfo(networkName string) (*network.Inspect, error) { | |
| 368 | + | networkInfo, err := dockerCli.NetworkInspect(context.Background(), networkName, network.InspectOptions{}) | |
| 355 | 369 | if err != nil { | |
| 356 | - | return types.NetworkResource{}, err | |
| 370 | + | return nil, err | |
| 357 | 371 | } | |
| 358 | 372 | ||
| 359 | - | return networkInfo, nil | |
| 373 | + | return &networkInfo, nil | |
| 360 | 374 | } | |
| 361 | 375 | ||
| 362 | 376 | // GetContainerInfo returns information about a container | |
| @@ -417,15 +431,16 @@ func UpdateContainerConfig(containerID string, newConfig *container.Config, newH | |||
| 417 | 431 | ||
| 418 | 432 | // GetContainerLogs returns the logs of a container | |
| 419 | 433 | func GetContainerLogs(containerID string) (string, error) { | |
| 420 | - | // Get container logs using the Docker client | |
| 421 | 434 | containerLogs, err := dockerCli.ContainerLogs(context.Background(), containerID, container.LogsOptions{ShowStdout: true, ShowStderr: true}) | |
| 422 | 435 | if err != nil { | |
| 423 | 436 | return "", err | |
| 424 | 437 | } | |
| 438 | + | defer containerLogs.Close() | |
| 425 | 439 | ||
| 426 | - | // Read the logs | |
| 427 | 440 | buf := new(bytes.Buffer) | |
| 428 | - | buf.ReadFrom(containerLogs) | |
| 441 | + | if _, err := buf.ReadFrom(containerLogs); err != nil { | |
| 442 | + | return "", fmt.Errorf("failed to read container logs: %w", err) | |
| 443 | + | } | |
| 429 | 444 | containerLogsString := buf.String() | |
| 430 | 445 | ||
| 431 | 446 | return containerLogsString, nil | |
| @@ -551,6 +566,15 @@ func GetContainerUptime(containerID string) (string, error) { | |||
| 551 | 566 | return formatDuration(duration), nil | |
| 552 | 567 | } | |
| 553 | 568 | ||
| 569 | + | // ContainerPortInfo holds information about container ports | |
| 570 | + | type ContainerPortInfo struct { | |
| 571 | + | Name string | |
| 572 | + | ExposedPorts map[nat.Port]struct{} | |
| 573 | + | PortBindings map[nat.Port][]nat.PortBinding | |
| 574 | + | PublishedPorts []types.Port | |
| 575 | + | } | |
| 576 | + | ||
| 577 | + | // GetContainersUsingPort returns a list of container names that are using the specified port | |
| 554 | 578 | func GetContainersUsingPort(port string) ([]string, error) { | |
| 555 | 579 | cli, err := client.NewClientWithOpts(client.FromEnv) | |
| 556 | 580 | if err != nil { | |
| @@ -561,59 +585,100 @@ func GetContainersUsingPort(port string) ([]string, error) { | |||
| 561 | 585 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | |
| 562 | 586 | defer cancel() | |
| 563 | 587 | ||
| 564 | - | // List all containers (including running and stopped) | |
| 565 | 588 | containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) | |
| 589 | + | if err != nil { | |
| 590 | + | return nil, fmt.Errorf("failed to list containers: %w", err) | |
| 591 | + | } | |
| 592 | + | ||
| 593 | + | return findContainersWithPort(ctx, cli, containers, port) | |
| 594 | + | } | |
| 595 | + | ||
| 596 | + | // findContainersWithPort processes the container list and finds those using the specified port | |
| 597 | + | func findContainersWithPort(ctx context.Context, cli *client.Client, containers []types.Container, port string) ([]string, error) { | |
| 566 | 598 | var containersUsingPort []string | |
| 599 | + | seen := make(map[string]bool) | |
| 567 | 600 | ||
| 568 | 601 | for _, container := range containers { | |
| 569 | - | // Inspect container to get detailed port information | |
| 570 | - | inspect, err := cli.ContainerInspect(ctx, container.ID) | |
| 602 | + | containerInfo, err := getContainerPortInfo(ctx, cli, container) | |
| 571 | 603 | if err != nil { | |
| 572 | - | continue // Skip containers we can't inspect | |
| 604 | + | log.Warn("Failed to inspect container", "container", container.ID, "error", err) | |
| 605 | + | continue | |
| 573 | 606 | } | |
| 574 | 607 | ||
| 575 | - | // Check exposed ports in container config | |
| 576 | - | if inspect.Config != nil && inspect.Config.ExposedPorts != nil { | |
| 577 | - | for exposedPort := range inspect.Config.ExposedPorts { | |
| 578 | - | if strings.HasPrefix(string(exposedPort), port+"/") { | |
| 579 | - | containersUsingPort = append(containersUsingPort, strings.TrimPrefix(container.Names[0], "/")) | |
| 580 | - | break | |
| 581 | - | } | |
| 582 | - | } | |
| 608 | + | if isContainerUsingPort(containerInfo, port) && !seen[containerInfo.Name] { | |
| 609 | + | seen[containerInfo.Name] = true | |
| 610 | + | containersUsingPort = append(containersUsingPort, containerInfo.Name) | |
| 583 | 611 | } | |
| 612 | + | } | |
| 584 | 613 | ||
| 585 | - | // Check port bindings in host config | |
| 586 | - | if inspect.HostConfig != nil && inspect.HostConfig.PortBindings != nil { | |
| 587 | - | for _, bindings := range inspect.HostConfig.PortBindings { | |
| 588 | - | for _, binding := range bindings { | |
| 589 | - | if binding.HostPort == port { | |
| 590 | - | containersUsingPort = append(containersUsingPort, strings.TrimPrefix(container.Names[0], "/")) | |
| 591 | - | break | |
| 592 | - | } | |
| 593 | - | } | |
| 594 | - | } | |
| 614 | + | return containersUsingPort, nil | |
| 615 | + | } | |
| 616 | + | ||
| 617 | + | // getContainerPortInfo retrieves port-related information for a container | |
| 618 | + | func getContainerPortInfo(ctx context.Context, cli *client.Client, container types.Container) (ContainerPortInfo, error) { | |
| 619 | + | inspect, err := cli.ContainerInspect(ctx, container.ID) | |
| 620 | + | if err != nil { | |
| 621 | + | return ContainerPortInfo{}, err | |
| 622 | + | } | |
| 623 | + | ||
| 624 | + | return ContainerPortInfo{ | |
| 625 | + | Name: strings.TrimPrefix(container.Names[0], "/"), | |
| 626 | + | ExposedPorts: inspect.Config.ExposedPorts, | |
| 627 | + | PortBindings: inspect.HostConfig.PortBindings, | |
| 628 | + | PublishedPorts: container.Ports, | |
| 629 | + | }, nil | |
| 630 | + | } | |
| 631 | + | ||
| 632 | + | // isContainerUsingPort checks if a container is using the specified port | |
| 633 | + | func isContainerUsingPort(info ContainerPortInfo, port string) bool { | |
| 634 | + | // Check exposed ports | |
| 635 | + | if hasExposedPort(info.ExposedPorts, port) { | |
| 636 | + | return true | |
| 637 | + | } | |
| 638 | + | ||
| 639 | + | // Check port bindings | |
| 640 | + | if hasPortBinding(info.PortBindings, port) { | |
| 641 | + | return true | |
| 642 | + | } | |
| 643 | + | ||
| 644 | + | // Check published ports | |
| 645 | + | if hasPublishedPort(info.PublishedPorts, port) { | |
| 646 | + | return true | |
| 647 | + | } | |
| 648 | + | ||
| 649 | + | return false | |
| 650 | + | } | |
| 651 | + | ||
| 652 | + | // hasExposedPort checks if the port is exposed in the container configuration | |
| 653 | + | func hasExposedPort(exposedPorts nat.PortSet, port string) bool { | |
| 654 | + | for exposedPort := range exposedPorts { | |
| 655 | + | if strings.HasPrefix(string(exposedPort), port+"/") { | |
| 656 | + | return true | |
| 595 | 657 | } | |
| 658 | + | } | |
| 659 | + | return false | |
| 660 | + | } | |
| 596 | 661 | ||
| 597 | - | // Check currently published ports | |
| 598 | - | for _, portBinding := range container.Ports { | |
| 599 | - | if fmt.Sprintf("%d", portBinding.PublicPort) == port { | |
| 600 | - | containersUsingPort = append(containersUsingPort, strings.TrimPrefix(container.Names[0], "/")) | |
| 601 | - | break | |
| 662 | + | // hasPortBinding checks if the port is bound in the host configuration | |
| 663 | + | func hasPortBinding(portBindings nat.PortMap, port string) bool { | |
| 664 | + | for _, bindings := range portBindings { | |
| 665 | + | for _, binding := range bindings { | |
| 666 | + | if binding.HostPort == port { | |
| 667 | + | return true | |
| 602 | 668 | } | |
| 603 | 669 | } | |
| 604 | 670 | } | |
| 671 | + | return false | |
| 672 | + | } | |
| 605 | 673 | ||
| 606 | - | // Remove duplicates | |
| 607 | - | seen := make(map[string]bool) | |
| 608 | - | unique := make([]string, 0, len(containersUsingPort)) | |
| 609 | - | for _, name := range containersUsingPort { | |
| 610 | - | if !seen[name] { | |
| 611 | - | seen[name] = true | |
| 612 | - | unique = append(unique, name) | |
| 674 | + | // hasPublishedPort checks if the port is currently published | |
| 675 | + | func hasPublishedPort(ports []types.Port, port string) bool { | |
| 676 | + | for _, portBinding := range ports { | |
| 677 | + | if fmt.Sprintf("%d", portBinding.PublicPort) == port { | |
| 678 | + | return true | |
| 613 | 679 | } | |
| 614 | 680 | } | |
| 615 | - | ||
| 616 | - | return unique, nil | |
| 681 | + | return false | |
| 617 | 682 | } | |
| 618 | 683 | ||
| 619 | 684 | func formatDuration(d time.Duration) string { | |
| @@ -109,8 +109,22 @@ func ListRunningContainers() ([]types.Container, error) { | |||||
| 109 | 109 | ||||
| 110 | // StopContainer try to stop a container gracefully, if it fails, it will stop it forcefully | 110 | // StopContainer try to stop a container gracefully, if it fails, it will stop it forcefully | ||
| 111 | func StopContainer(containerID string) error { | 111 | func StopContainer(containerID string) error { | ||
| 112 | - | StopContainerGracefully(containerID, 3*time.Second) | 112 | + | // Try graceful shutdown first |
| 113 | - | StopContainerRagefully(containerID) | 113 | + | stopped, err := StopContainerGracefully(containerID, 3*time.Second) |
| 114 | + | if err != nil { | |||
| 115 | + | log.Warn("Failed to stop container gracefully", "error", err) | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | // If container wasn't stopped gracefully or there was an error, try forceful shutdown | |||
| 119 | + | if !stopped { | |||
| 120 | + | if err := StopContainerRagefully(containerID); err != nil { | |||
| 121 | + | return fmt.Errorf("failed to stop container forcefully: %w", err) | |||
| 122 | + | } | |||
| 123 | + | log.Info("Container stopped forcefully", "containerID", containerID) | |||
| 124 | + | } else { | |||
| 125 | + | log.Info("Container stopped gracefully", "containerID", containerID) | |||
| 126 | + | } | |||
| 127 | + | ||||
| 114 | return nil | 128 | return nil | ||
| 115 | } | 129 | } | ||
| 116 | 130 | ||||
| @@ -127,18 +141,16 @@ func StopContainerGracefully(containerID string, timeoutDuration time.Duration) | |||||
| 127 | defer ticker.Stop() | 141 | defer ticker.Stop() | ||
| 128 | 142 | ||||
| 129 | for elapsed := 0; elapsed < int(timeoutDuration.Seconds()); elapsed++ { | 143 | for elapsed := 0; elapsed < int(timeoutDuration.Seconds()); elapsed++ { | ||
| 130 | - | select { | 144 | + | <-ticker.C |
| 131 | - | case <-ticker.C: | 145 | + | // Check if the container is still running |
| 132 | - | // Check if the container is still running | 146 | + | container, err := dockerCli.ContainerInspect(context.Background(), containerID) |
| 133 | - | container, err := dockerCli.ContainerInspect(context.Background(), containerID) | 147 | + | if err != nil { |
| 134 | - | if err != nil { | 148 | + | return false, err |
| 135 | - | return false, err | 149 | + | } |
| 136 | - | } | |||
| 137 | 150 | ||||
| 138 | - | // If the container is not running, return true indicating it was stopped | 151 | + | // If the container is not running, return true |
| 139 | - | if !container.State.Running { | 152 | + | if !container.State.Running { |
| 140 | - | return true, nil | 153 | + | return true, nil |
| 141 | - | } | |||
| 142 | } | 154 | } | ||
| 143 | } | 155 | } | ||
| 144 | 156 | ||||
| @@ -229,7 +241,10 @@ func StartContainer(containerID string) error { | |||||
| 229 | 241 | ||||
| 230 | // CreateContainer creates a container with the given parameters | 242 | // CreateContainer creates a container with the given parameters | ||
| 231 | func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | 243 | func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | ||
| 232 | - | CheckIfInitialized() | 244 | + | |
| 245 | + | if err := CheckIfInitialized(); err != nil { | |||
| 246 | + | return "", fmt.Errorf("failed to check if docker client is initialized: %w", err) | |||
| 247 | + | } | |||
| 233 | 248 | ||||
| 234 | log.Info("Creating container with params: %+v\n", cmdParams) | 249 | log.Info("Creating container with params: %+v\n", cmdParams) | ||
| 235 | 250 | ||||
| @@ -241,7 +256,7 @@ func CreateContainer(cmdParams ContainerCommandParams) (string, error) { | |||||
| 241 | 256 | ||||
| 242 | if !isNetworkCreated { | 257 | if !isNetworkCreated { | ||
| 243 | fmt.Printf("Creating network: %s\n", cmdParams.Network) | 258 | fmt.Printf("Creating network: %s\n", cmdParams.Network) | ||
| 244 | - | _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, types.NetworkCreate{}) | 259 | + | _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, network.CreateOptions{}) |
| 245 | if err != nil { | 260 | if err != nil { | ||
| 246 | return "", fmt.Errorf("network creation failed: %v", err) | 261 | return "", fmt.Errorf("network creation failed: %v", err) | ||
| 247 | } | 262 | } | ||
| @@ -334,7 +349,7 @@ func WaitForContainerToBeRunning(containerID string, timeout time.Duration) erro | |||||
| 334 | } | 349 | } | ||
| 335 | 350 | ||||
| 336 | func CheckIfNetworkExists(networkName string) (bool, error) { | 351 | func CheckIfNetworkExists(networkName string) (bool, error) { | ||
| 337 | - | networks, err := dockerCli.NetworkList(context.Background(), types.NetworkListOptions{}) | 352 | + | networks, err := dockerCli.NetworkList(context.Background(), network.ListOptions{}) |
| 338 | if err != nil { | 353 | if err != nil { | ||
| 339 | return false, err | 354 | return false, err | ||
| 340 | } | 355 | } | ||
| @@ -349,14 +364,13 @@ func CheckIfNetworkExists(networkName string) (bool, error) { | |||||
| 349 | } | 364 | } | ||
| 350 | 365 | ||||
| 351 | // GetNetworkInfo returns information about a network | 366 | // GetNetworkInfo returns information about a network | ||
| 352 | - | func GetNetworkInfo(networkName string) (types.NetworkResource, error) { | 367 | + | func GetNetworkInfo(networkName string) (*network.Inspect, error) { |
| 353 | - | // Get network info using the Docker client | 368 | + | networkInfo, err := dockerCli.NetworkInspect(context.Background(), networkName, network.InspectOptions{}) |
| 354 | - | networkInfo, err := dockerCli.NetworkInspect(context.Background(), networkName, types.NetworkInspectOptions{}) | |||
| 355 | if err != nil { | 369 | if err != nil { | ||
| 356 | - | return types.NetworkResource{}, err | 370 | + | return nil, err |
| 357 | } | 371 | } | ||
| 358 | 372 | ||||
| 359 | - | return networkInfo, nil | 373 | + | return &networkInfo, nil |
| 360 | } | 374 | } | ||
| 361 | 375 | ||||
| 362 | // GetContainerInfo returns information about a container | 376 | // GetContainerInfo returns information about a container | ||
| @@ -417,15 +431,16 @@ func UpdateContainerConfig(containerID string, newConfig *container.Config, newH | |||||
| 417 | 431 | ||||
| 418 | // GetContainerLogs returns the logs of a container | 432 | // GetContainerLogs returns the logs of a container | ||
| 419 | func GetContainerLogs(containerID string) (string, error) { | 433 | func GetContainerLogs(containerID string) (string, error) { | ||
| 420 | - | // Get container logs using the Docker client | |||
| 421 | containerLogs, err := dockerCli.ContainerLogs(context.Background(), containerID, container.LogsOptions{ShowStdout: true, ShowStderr: true}) | 434 | containerLogs, err := dockerCli.ContainerLogs(context.Background(), containerID, container.LogsOptions{ShowStdout: true, ShowStderr: true}) | ||
| 422 | if err != nil { | 435 | if err != nil { | ||
| 423 | return "", err | 436 | return "", err | ||
| 424 | } | 437 | } | ||
| 438 | + | defer containerLogs.Close() | |||
| 425 | 439 | ||||
| 426 | - | // Read the logs | |||
| 427 | buf := new(bytes.Buffer) | 440 | buf := new(bytes.Buffer) | ||
| 428 | - | buf.ReadFrom(containerLogs) | 441 | + | if _, err := buf.ReadFrom(containerLogs); err != nil { |
| 442 | + | return "", fmt.Errorf("failed to read container logs: %w", err) | |||
| 443 | + | } | |||
| 429 | containerLogsString := buf.String() | 444 | containerLogsString := buf.String() | ||
| 430 | 445 | ||||
| 431 | return containerLogsString, nil | 446 | return containerLogsString, nil | ||
| @@ -551,6 +566,15 @@ func GetContainerUptime(containerID string) (string, error) { | |||||
| 551 | return formatDuration(duration), nil | 566 | return formatDuration(duration), nil | ||
| 552 | } | 567 | } | ||
| 553 | 568 | ||||
| 569 | + | // ContainerPortInfo holds information about container ports | |||
| 570 | + | type ContainerPortInfo struct { | |||
| 571 | + | Name string | |||
| 572 | + | ExposedPorts map[nat.Port]struct{} | |||
| 573 | + | PortBindings map[nat.Port][]nat.PortBinding | |||
| 574 | + | PublishedPorts []types.Port | |||
| 575 | + | } | |||
| 576 | + | ||||
| 577 | + | // GetContainersUsingPort returns a list of container names that are using the specified port | |||
| 554 | func GetContainersUsingPort(port string) ([]string, error) { | 578 | func GetContainersUsingPort(port string) ([]string, error) { | ||
| 555 | cli, err := client.NewClientWithOpts(client.FromEnv) | 579 | cli, err := client.NewClientWithOpts(client.FromEnv) | ||
| 556 | if err != nil { | 580 | if err != nil { | ||
| @@ -561,59 +585,100 @@ func GetContainersUsingPort(port string) ([]string, error) { | |||||
| 561 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | 585 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | ||
| 562 | defer cancel() | 586 | defer cancel() | ||
| 563 | 587 | ||||
| 564 | - | // List all containers (including running and stopped) | |||
| 565 | containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) | 588 | containers, err := cli.ContainerList(ctx, container.ListOptions{All: true}) | ||
| 589 | + | if err != nil { | |||
| 590 | + | return nil, fmt.Errorf("failed to list containers: %w", err) | |||
| 591 | + | } | |||
| 592 | + | ||||
| 593 | + | return findContainersWithPort(ctx, cli, containers, port) | |||
| 594 | + | } | |||
| 595 | + | ||||
| 596 | + | // findContainersWithPort processes the container list and finds those using the specified port | |||
| 597 | + | func findContainersWithPort(ctx context.Context, cli *client.Client, containers []types.Container, port string) ([]string, error) { | |||
| 566 | var containersUsingPort []string | 598 | var containersUsingPort []string | ||
| 599 | + | seen := make(map[string]bool) | |||
| 567 | 600 | ||||
| 568 | for _, container := range containers { | 601 | for _, container := range containers { | ||
| 569 | - | // Inspect container to get detailed port information | 602 | + | containerInfo, err := getContainerPortInfo(ctx, cli, container) |
| 570 | - | inspect, err := cli.ContainerInspect(ctx, container.ID) | |||
| 571 | if err != nil { | 603 | if err != nil { | ||
| 572 | - | continue // Skip containers we can't inspect | 604 | + | log.Warn("Failed to inspect container", "container", container.ID, "error", err) |
| 605 | + | continue | |||
| 573 | } | 606 | } | ||
| 574 | 607 | ||||
| 575 | - | // Check exposed ports in container config | 608 | + | if isContainerUsingPort(containerInfo, port) && !seen[containerInfo.Name] { |
| 576 | - | if inspect.Config != nil && inspect.Config.ExposedPorts != nil { | 609 | + | seen[containerInfo.Name] = true |
| 577 | - | for exposedPort := range inspect.Config.ExposedPorts { | 610 | + | containersUsingPort = append(containersUsingPort, containerInfo.Name) |
| 578 | - | if strings.HasPrefix(string(exposedPort), port+"/") { | |||
| 579 | - | containersUsingPort = append(containersUsingPort, strings.TrimPrefix(container.Names[0], "/")) | |||
| 580 | - | break | |||
| 581 | - | } | |||
| 582 | - | } | |||
| 583 | } | 611 | } | ||
| 612 | + | } | |||
| 584 | 613 | ||||
| 585 | - | // Check port bindings in host config | 614 | + | return containersUsingPort, nil |
| 586 | - | if inspect.HostConfig != nil && inspect.HostConfig.PortBindings != nil { | 615 | + | } |
| 587 | - | for _, bindings := range inspect.HostConfig.PortBindings { | 616 | + | |
| 588 | - | for _, binding := range bindings { | 617 | + | // getContainerPortInfo retrieves port-related information for a container |
| 589 | - | if binding.HostPort == port { | 618 | + | func getContainerPortInfo(ctx context.Context, cli *client.Client, container types.Container) (ContainerPortInfo, error) { |
| 590 | - | containersUsingPort = append(containersUsingPort, strings.TrimPrefix(container.Names[0], "/")) | 619 | + | inspect, err := cli.ContainerInspect(ctx, container.ID) |
| 591 | - | break | 620 | + | if err != nil { |
| 592 | - | } | 621 | + | return ContainerPortInfo{}, err |
| 593 | - | } | 622 | + | } |
| 594 | - | } | 623 | + | |
| 624 | + | return ContainerPortInfo{ | |||
| 625 | + | Name: strings.TrimPrefix(container.Names[0], "/"), | |||
| 626 | + | ExposedPorts: inspect.Config.ExposedPorts, | |||
| 627 | + | PortBindings: inspect.HostConfig.PortBindings, | |||
| 628 | + | PublishedPorts: container.Ports, | |||
| 629 | + | }, nil | |||
| 630 | + | } | |||
| 631 | + | ||||
| 632 | + | // isContainerUsingPort checks if a container is using the specified port | |||
| 633 | + | func isContainerUsingPort(info ContainerPortInfo, port string) bool { | |||
| 634 | + | // Check exposed ports | |||
| 635 | + | if hasExposedPort(info.ExposedPorts, port) { | |||
| 636 | + | return true | |||
| 637 | + | } | |||
| 638 | + | ||||
| 639 | + | // Check port bindings | |||
| 640 | + | if hasPortBinding(info.PortBindings, port) { | |||
| 641 | + | return true | |||
| 642 | + | } | |||
| 643 | + | ||||
| 644 | + | // Check published ports | |||
| 645 | + | if hasPublishedPort(info.PublishedPorts, port) { | |||
| 646 | + | return true | |||
| 647 | + | } | |||
| 648 | + | ||||
| 649 | + | return false | |||
| 650 | + | } | |||
| 651 | + | ||||
| 652 | + | // hasExposedPort checks if the port is exposed in the container configuration | |||
| 653 | + | func hasExposedPort(exposedPorts nat.PortSet, port string) bool { | |||
| 654 | + | for exposedPort := range exposedPorts { | |||
| 655 | + | if strings.HasPrefix(string(exposedPort), port+"/") { | |||
| 656 | + | return true | |||
| 595 | } | 657 | } | ||
| 658 | + | } | |||
| 659 | + | return false | |||
| 660 | + | } | |||
| 596 | 661 | ||||
| 597 | - | // Check currently published ports | 662 | + | // hasPortBinding checks if the port is bound in the host configuration |
| 598 | - | for _, portBinding := range container.Ports { | 663 | + | func hasPortBinding(portBindings nat.PortMap, port string) bool { |
| 599 | - | if fmt.Sprintf("%d", portBinding.PublicPort) == port { | 664 | + | for _, bindings := range portBindings { |
| 600 | - | containersUsingPort = append(containersUsingPort, strings.TrimPrefix(container.Names[0], "/")) | 665 | + | for _, binding := range bindings { |
| 601 | - | break | 666 | + | if binding.HostPort == port { |
| 667 | + | return true | |||
| 602 | } | 668 | } | ||
| 603 | } | 669 | } | ||
| 604 | } | 670 | } | ||
| 671 | + | return false | |||
| 672 | + | } | |||
| 605 | 673 | ||||
| 606 | - | // Remove duplicates | 674 | + | // hasPublishedPort checks if the port is currently published |
| 607 | - | seen := make(map[string]bool) | 675 | + | func hasPublishedPort(ports []types.Port, port string) bool { |
| 608 | - | unique := make([]string, 0, len(containersUsingPort)) | 676 | + | for _, portBinding := range ports { |
| 609 | - | for _, name := range containersUsingPort { | 677 | + | if fmt.Sprintf("%d", portBinding.PublicPort) == port { |
| 610 | - | if !seen[name] { | 678 | + | return true |
| 611 | - | seen[name] = true | |||
| 612 | - | unique = append(unique, name) | |||
| 613 | } | 679 | } | ||
| 614 | } | 680 | } | ||
| 615 | - | 681 | + | return false | |
| 616 | - | return unique, nil | |||
| 617 | } | 682 | } | ||
| 618 | 683 | ||||
| 619 | func formatDuration(d time.Duration) string { | 684 | func formatDuration(d time.Duration) string { | ||
M pkg/docker/images.go +3 -1
| @@ -18,7 +18,9 @@ import ( | |||
| 18 | 18 | ||
| 19 | 19 | func ListContainerImages() ([]image.Summary, error) { | |
| 20 | 20 | // Check if the Docker client has been initialized | |
| 21 | - | CheckIfInitialized() | |
| 21 | + | if err := CheckIfInitialized(); err != nil { | |
| 22 | + | return nil, fmt.Errorf("failed to initialize Docker client: %w", err) | |
| 23 | + | } | |
| 22 | 24 | ||
| 23 | 25 | // List images using the Docker client | |
| 24 | 26 | images, err := dockerCli.ImageList(context.Background(), image.ListOptions{}) | |
| @@ -18,7 +18,9 @@ import ( | |||||
| 18 | 18 | ||||
| 19 | func ListContainerImages() ([]image.Summary, error) { | 19 | func ListContainerImages() ([]image.Summary, error) { | ||
| 20 | // Check if the Docker client has been initialized | 20 | // Check if the Docker client has been initialized | ||
| 21 | - | CheckIfInitialized() | 21 | + | if err := CheckIfInitialized(); err != nil { |
| 22 | + | return nil, fmt.Errorf("failed to initialize Docker client: %w", err) | |||
| 23 | + | } | |||
| 22 | 24 | ||||
| 23 | // List images using the Docker client | 25 | // List images using the Docker client | ||
| 24 | images, err := dockerCli.ImageList(context.Background(), image.ListOptions{}) | 26 | images, err := dockerCli.ImageList(context.Background(), image.ListOptions{}) | ||
M pkg/parser/yaml.go +1 -1
| @@ -47,7 +47,7 @@ func WriteYAMLFile(filename string, in interface{}) error { | |||
| 47 | 47 | } | |
| 48 | 48 | ||
| 49 | 49 | // Write the YAML data to the file | |
| 50 | - | err = os.WriteFile(filename, data, 0644) | |
| 50 | + | err = os.WriteFile(filename, data, 0600) | |
| 51 | 51 | if err != nil { | |
| 52 | 52 | return fmt.Errorf("failed to write to %s: %w", filename, err) | |
| 53 | 53 | } | |
| @@ -47,7 +47,7 @@ func WriteYAMLFile(filename string, in interface{}) error { | |||||
| 47 | } | 47 | } | ||
| 48 | 48 | ||||
| 49 | // Write the YAML data to the file | 49 | // Write the YAML data to the file | ||
| 50 | - | err = os.WriteFile(filename, data, 0644) | 50 | + | err = os.WriteFile(filename, data, 0600) |
| 51 | if err != nil { | 51 | if err != nil { | ||
| 52 | return fmt.Errorf("failed to write to %s: %w", filename, err) | 52 | return fmt.Errorf("failed to write to %s: %w", filename, err) | ||
| 53 | } | 53 | } | ||
M pkg/store/store_images.go +14 -30
| @@ -5,36 +5,17 @@ import ( | |||
| 5 | 5 | "io" | |
| 6 | 6 | "os" | |
| 7 | 7 | "path/filepath" | |
| 8 | - | "sync" | |
| 9 | 8 | ||
| 10 | 9 | "github.com/bnema/gordon/internal/common" | |
| 10 | + | "github.com/charmbracelet/log" | |
| 11 | 11 | "github.com/google/uuid" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| 14 | - | var ( | |
| 15 | - | imagePath string | |
| 16 | - | mu sync.Mutex | |
| 17 | - | ) | |
| 18 | - | ||
| 19 | - | // setImagePath sets the image path in a thread-safe manner | |
| 20 | - | func setImagePath(path string) { | |
| 21 | - | mu.Lock() | |
| 22 | - | defer mu.Unlock() | |
| 23 | - | imagePath = path | |
| 24 | - | } | |
| 25 | - | ||
| 26 | - | // getImagePath gets the image path in a thread-safe manner | |
| 27 | - | func getImagePath() string { | |
| 28 | - | mu.Lock() | |
| 29 | - | defer mu.Unlock() | |
| 30 | - | return imagePath | |
| 31 | - | } | |
| 32 | - | ||
| 33 | 14 | func SaveImageToStorage(config *common.Config, originalFilename string, buf io.Reader) (string, error) { | |
| 34 | 15 | // Ensure the images directory exists | |
| 35 | 16 | imagesDir := filepath.Join(config.General.StorageDir, "images") | |
| 36 | 17 | if err := os.MkdirAll(imagesDir, 0755); err != nil { | |
| 37 | - | return "", fmt.Errorf("failed to create images directory: %v", err) | |
| 18 | + | return "", fmt.Errorf("failed to create images directory: %w", err) | |
| 38 | 19 | } | |
| 39 | 20 | ||
| 40 | 21 | // Generate a unique ID for the image | |
| @@ -42,17 +23,17 @@ func SaveImageToStorage(config *common.Config, originalFilename string, buf io.R | |||
| 42 | 23 | ||
| 43 | 24 | // Create a filename with the ID and original extension | |
| 44 | 25 | ext := filepath.Ext(originalFilename) | |
| 45 | - | filename := fmt.Sprintf("%s%s", id, ext) | |
| 26 | + | filename := id + ext | |
| 46 | 27 | ||
| 47 | 28 | // Full path for the new image | |
| 48 | 29 | imagePath := filepath.Join(imagesDir, filename) | |
| 49 | 30 | ||
| 50 | - | fmt.Printf("Saving image to: %s\n", imagePath) | |
| 31 | + | log.Info("saving image", "path", imagePath) | |
| 51 | 32 | ||
| 52 | 33 | // Create the file | |
| 53 | 34 | outFile, err := os.Create(imagePath) | |
| 54 | 35 | if err != nil { | |
| 55 | - | return "", fmt.Errorf("failed to create file: %v", err) | |
| 36 | + | return "", fmt.Errorf("failed to create file: %w", err) | |
| 56 | 37 | } | |
| 57 | 38 | defer outFile.Close() | |
| 58 | 39 | ||
| @@ -60,27 +41,30 @@ func SaveImageToStorage(config *common.Config, originalFilename string, buf io.R | |||
| 60 | 41 | written, err := io.Copy(outFile, buf) | |
| 61 | 42 | if err != nil { | |
| 62 | 43 | os.Remove(imagePath) // Clean up in case of error | |
| 63 | - | return "", fmt.Errorf("failed to write file: %v", err) | |
| 44 | + | return "", fmt.Errorf("failed to write file: %w", err) | |
| 64 | 45 | } | |
| 65 | 46 | ||
| 66 | - | fmt.Printf("Wrote %d bytes to %s\n", written, imagePath) | |
| 47 | + | log.Info("file written successfully", | |
| 48 | + | "bytes", written, | |
| 49 | + | "path", imagePath, | |
| 50 | + | ) | |
| 67 | 51 | ||
| 68 | 52 | return imagePath, nil | |
| 69 | 53 | } | |
| 70 | 54 | ||
| 71 | 55 | func RemoveFromStorage(imagePath string) error { | |
| 72 | 56 | if imagePath == "" { | |
| 73 | - | return fmt.Errorf("no image path provided to remove") | |
| 57 | + | return fmt.Errorf("empty image path") | |
| 74 | 58 | } | |
| 75 | 59 | ||
| 76 | - | fmt.Printf("Attempting to remove file: %s\n", imagePath) | |
| 60 | + | log.Info("attempting to remove file", "path", imagePath) | |
| 77 | 61 | ||
| 78 | 62 | err := os.Remove(imagePath) | |
| 79 | 63 | if err != nil { | |
| 80 | - | return fmt.Errorf("failed to remove file: %v", err) | |
| 64 | + | return fmt.Errorf("failed to remove file: %w", err) | |
| 81 | 65 | } | |
| 82 | 66 | ||
| 83 | - | fmt.Printf("Successfully removed file: %s\n", imagePath) | |
| 67 | + | log.Info("file removed successfully", "path", imagePath) | |
| 84 | 68 | ||
| 85 | 69 | return nil | |
| 86 | 70 | } | |
| @@ -5,36 +5,17 @@ import ( | |||||
| 5 | "io" | 5 | "io" | ||
| 6 | "os" | 6 | "os" | ||
| 7 | "path/filepath" | 7 | "path/filepath" | ||
| 8 | - | "sync" | |||
| 9 | 8 | ||||
| 10 | "github.com/bnema/gordon/internal/common" | 9 | "github.com/bnema/gordon/internal/common" | ||
| 10 | + | "github.com/charmbracelet/log" | |||
| 11 | "github.com/google/uuid" | 11 | "github.com/google/uuid" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
| 14 | - | var ( | |||
| 15 | - | imagePath string | |||
| 16 | - | mu sync.Mutex | |||
| 17 | - | ) | |||
| 18 | - | ||||
| 19 | - | // setImagePath sets the image path in a thread-safe manner | |||
| 20 | - | func setImagePath(path string) { | |||
| 21 | - | mu.Lock() | |||
| 22 | - | defer mu.Unlock() | |||
| 23 | - | imagePath = path | |||
| 24 | - | } | |||
| 25 | - | ||||
| 26 | - | // getImagePath gets the image path in a thread-safe manner | |||
| 27 | - | func getImagePath() string { | |||
| 28 | - | mu.Lock() | |||
| 29 | - | defer mu.Unlock() | |||
| 30 | - | return imagePath | |||
| 31 | - | } | |||
| 32 | - | ||||
| 33 | func SaveImageToStorage(config *common.Config, originalFilename string, buf io.Reader) (string, error) { | 14 | func SaveImageToStorage(config *common.Config, originalFilename string, buf io.Reader) (string, error) { | ||
| 34 | // Ensure the images directory exists | 15 | // Ensure the images directory exists | ||
| 35 | imagesDir := filepath.Join(config.General.StorageDir, "images") | 16 | imagesDir := filepath.Join(config.General.StorageDir, "images") | ||
| 36 | if err := os.MkdirAll(imagesDir, 0755); err != nil { | 17 | if err := os.MkdirAll(imagesDir, 0755); err != nil { | ||
| 37 | - | return "", fmt.Errorf("failed to create images directory: %v", err) | 18 | + | return "", fmt.Errorf("failed to create images directory: %w", err) |
| 38 | } | 19 | } | ||
| 39 | 20 | ||||
| 40 | // Generate a unique ID for the image | 21 | // Generate a unique ID for the image | ||
| @@ -42,17 +23,17 @@ func SaveImageToStorage(config *common.Config, originalFilename string, buf io.R | |||||
| 42 | 23 | ||||
| 43 | // Create a filename with the ID and original extension | 24 | // Create a filename with the ID and original extension | ||
| 44 | ext := filepath.Ext(originalFilename) | 25 | ext := filepath.Ext(originalFilename) | ||
| 45 | - | filename := fmt.Sprintf("%s%s", id, ext) | 26 | + | filename := id + ext |
| 46 | 27 | ||||
| 47 | // Full path for the new image | 28 | // Full path for the new image | ||
| 48 | imagePath := filepath.Join(imagesDir, filename) | 29 | imagePath := filepath.Join(imagesDir, filename) | ||
| 49 | 30 | ||||
| 50 | - | fmt.Printf("Saving image to: %s\n", imagePath) | 31 | + | log.Info("saving image", "path", imagePath) |
| 51 | 32 | ||||
| 52 | // Create the file | 33 | // Create the file | ||
| 53 | outFile, err := os.Create(imagePath) | 34 | outFile, err := os.Create(imagePath) | ||
| 54 | if err != nil { | 35 | if err != nil { | ||
| 55 | - | return "", fmt.Errorf("failed to create file: %v", err) | 36 | + | return "", fmt.Errorf("failed to create file: %w", err) |
| 56 | } | 37 | } | ||
| 57 | defer outFile.Close() | 38 | defer outFile.Close() | ||
| 58 | 39 | ||||
| @@ -60,27 +41,30 @@ func SaveImageToStorage(config *common.Config, originalFilename string, buf io.R | |||||
| 60 | written, err := io.Copy(outFile, buf) | 41 | written, err := io.Copy(outFile, buf) | ||
| 61 | if err != nil { | 42 | if err != nil { | ||
| 62 | os.Remove(imagePath) // Clean up in case of error | 43 | os.Remove(imagePath) // Clean up in case of error | ||
| 63 | - | return "", fmt.Errorf("failed to write file: %v", err) | 44 | + | return "", fmt.Errorf("failed to write file: %w", err) |
| 64 | } | 45 | } | ||
| 65 | 46 | ||||
| 66 | - | fmt.Printf("Wrote %d bytes to %s\n", written, imagePath) | 47 | + | log.Info("file written successfully", |
| 48 | + | "bytes", written, | |||
| 49 | + | "path", imagePath, | |||
| 50 | + | ) | |||
| 67 | 51 | ||||
| 68 | return imagePath, nil | 52 | return imagePath, nil | ||
| 69 | } | 53 | } | ||
| 70 | 54 | ||||
| 71 | func RemoveFromStorage(imagePath string) error { | 55 | func RemoveFromStorage(imagePath string) error { | ||
| 72 | if imagePath == "" { | 56 | if imagePath == "" { | ||
| 73 | - | return fmt.Errorf("no image path provided to remove") | 57 | + | return fmt.Errorf("empty image path") |
| 74 | } | 58 | } | ||
| 75 | 59 | ||||
| 76 | - | fmt.Printf("Attempting to remove file: %s\n", imagePath) | 60 | + | log.Info("attempting to remove file", "path", imagePath) |
| 77 | 61 | ||||
| 78 | err := os.Remove(imagePath) | 62 | err := os.Remove(imagePath) | ||
| 79 | if err != nil { | 63 | if err != nil { | ||
| 80 | - | return fmt.Errorf("failed to remove file: %v", err) | 64 | + | return fmt.Errorf("failed to remove file: %w", err) |
| 81 | } | 65 | } | ||
| 82 | 66 | ||||
| 83 | - | fmt.Printf("Successfully removed file: %s\n", imagePath) | 67 | + | log.Info("file removed successfully", "path", imagePath) |
| 84 | 68 | ||||
| 85 | return nil | 69 | return nil | ||
| 86 | } | 70 | } | ||