Skip to content
This repository was archived by the owner on Sep 9, 2025. It is now read-only.

Conversation

@kmendell
Copy link
Member

@kmendell kmendell commented Jun 21, 2025

Summary by CodeRabbit

  • New Features

    • Introduced a RESTful HTTP API server for managing Docker containers, images, networks, volumes, and compose stacks.
    • Added endpoints for real-time streaming of container stats and stack logs.
    • Implemented API key authentication and Docker availability middleware.
    • Provided data models and DTOs for stack and image operations.
  • Improvements

    • Refactored Docker integration to use the official Docker Go SDK for all operations.
    • Simplified and validated configuration loading with enhanced agent status tracking.
    • Improved logging and graceful shutdown for the agent service.
  • Bug Fixes

    • Enhanced error handling and response consistency across API endpoints.
  • Chores

    • Updated dependency management and provided an example environment configuration file.
  • Tests

    • Removed legacy and now-obsolete tests following the architectural overhaul.
  • Refactor

    • Streamlined the codebase by removing deprecated task, compose, and messaging modules.

@coderabbitai
Copy link

coderabbitai bot commented Jun 21, 2025

📝 Walkthrough

Walkthrough

This update restructures the codebase by removing legacy task management, Compose management, and HTTP client code, replacing them with a new RESTful API architecture built on the Gin framework. It introduces new handler, service, model, and middleware layers for Docker and stack operations, refactors the Docker client to use the official SDK, and streamlines configuration management. Numerous test files and obsolete logic are deleted, while new DTOs and models are added to support the revised API.

Changes

Files / Groups Change Summary
.env.example Added example environment variable template for agent configuration.
Dockerfile Changed exposed port from 8080 to 3552.
cmd/agent/main.go Clarified .env load log message and renamed agent variable for clarity.
cmd/agent/main_test.go Deleted compilation test for main package.
go.mod Organized and expanded dependency declarations, separating direct and indirect dependencies.
internal/agent/agent.go Refactored Agent to embed an HTTP API server, manage lifecycle status, and improve Docker client error handling.
internal/agent/agent_test.go Deleted unit tests for agent creation and lifecycle management.
internal/agent/http_client.go, internal/agent/http_client_test.go Deleted HTTP client logic for agent-server communication and its tests.
internal/api/router.go Added Gin router setup with grouped RESTful endpoints for Docker and stack resources.
internal/compose/manager.go, internal/compose/manager_test.go Deleted Compose project management logic and its tests.
internal/config/config.go Simplified config struct, removed file-based agent ID persistence, and improved validation.
internal/config/config_test.go Deleted configuration loading and helper function tests.
internal/docker/client.go Refactored to use Docker Go SDK for all operations, removed CLI command execution, and added new resource management methods.
internal/docker/client_test.go Deleted Docker client unit tests.
internal/dto/image_dto.go Added DTO struct for image pull operations.
internal/dto/stack.go Added DTO structs for stack creation, update, redeploy, and destroy operations.
internal/handlers/container_handler.go Added container management HTTP handlers (list, get, start, stop, restart, stats, streaming stats).
internal/handlers/docker_handler.go Added Docker system info HTTP handler.
internal/handlers/image_handler.go Added image management HTTP handlers (list, get, pull, create, delete, tag, push).
internal/handlers/network_handler.go Added network management HTTP handlers (list, get, create, delete, connect, disconnect, prune).
internal/handlers/stack_handler.go Added stack management HTTP handlers (CRUD, lifecycle, logs streaming, image pull, deploy, convert).
internal/handlers/status_handler.go Added status endpoint HTTP handler.
internal/handlers/volume_handler.go Added volume management HTTP handlers (list, get, usage, create, delete, prune).
internal/middleware/middleware.go Added API key and Docker availability middleware for Gin.
internal/models/stack.go Added stack, service, port, network, and conversion request/response models.
internal/models/stack_service_info.go Added StackServiceInfo struct for stack service metadata.
internal/services/container.go Added container stats service with single and streaming stats methods.
internal/services/stack.go Added stack service for managing Compose stacks via filesystem and CLI, including logs streaming and service info parsing.
internal/tasks/docker.go, internal/tasks/manager.go, internal/tasks/manager_test.go, internal/tasks/system.go Deleted legacy Docker task executor, task manager, system task executor, and all related tests.
pkg/types/message.go, pkg/types/message_test.go Deleted message, task, and metrics struct definitions and their serialization tests.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai auto-generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🧹 Nitpick comments (11)
cmd/agent/main.go (2)

16-18: Clarify non-fatal .env load behavior.

The updated log message clearly indicates that a missing .env file is acceptable. Consider demoting this to log.Debug if a debug level is available to avoid noise in production logs.


25-26: Use a more descriptive variable name than a single letter.

Naming the agent instance a may hinder readability. Consider a more descriptive identifier (e.g., arcaneAgent or svc) for clarity.

internal/services/container.go (1)

22-67: Consider using typed stats instead of interface{}

Both methods return interface{} for stats data, which reduces type safety and makes it harder for callers to work with the data.

Define a proper stats type or use Docker's types directly:

import "github.com/docker/docker/api/types"

func (s *ContainerService) GetStats(ctx context.Context, containerID string, stream bool) (*types.StatsJSON, error) {
	// ... existing code ...
	var statsData types.StatsJSON
	decoder := json.NewDecoder(stats.Body)
	if err := decoder.Decode(&statsData); err != nil {
		return nil, fmt.Errorf("failed to decode stats: %w", err)
	}
	return &statsData, nil
}
internal/config/config.go (1)

29-40: Consider validating or warning about empty API key

The API key is loaded from environment but not validated. An empty API key could lead to authentication issues if the middleware expects it.

Add validation or at least log a warning:

 cfg := &Config{
 	AgentID:            agentID,
 	Version:            version.GetVersion(),
 	AgentListenAddress: getEnv("AGENT_LISTEN_ADDRESS", "0.0.0.0"),
 	AgentPort:          getEnvInt("AGENT_PORT", 3552),
 	APIKey:             getEnv("API_KEY", ""),
 }
+
+// Log warning if API key is not set
+if cfg.APIKey == "" {
+	// Consider if this should be a warning or an error based on your security requirements
+	fmt.Println("Warning: API_KEY not set - API endpoints may be unprotected")
+}
internal/handlers/volume_handler.go (1)

72-72: Use http.StatusOK constant for consistency

For consistency with other handlers, use the HTTP status constant instead of hardcoded value.

-	c.JSON(200, gin.H{
+	c.JSON(http.StatusOK, gin.H{
internal/api/router.go (1)

20-35: Consider consistent handler initialization pattern.

Some handlers are initialized as variables while others (network and volume) are created inline. For better maintainability, consider initializing all handlers consistently.

 // Initialize handlers
 statusHandler := handlers.NewStatusHandler(cfg)
 containerHandler := handlers.NewContainerHandler(dockerClient)
 dockerHandler := handlers.NewDockerHandler(dockerClient)
 imageHandler := handlers.NewImageHandler(dockerClient)
 stackHandler := handlers.NewStackHandler(services.NewStackService())
+networkHandler := handlers.NewNetworkHandler(dockerClient)
+volumeHandler := handlers.NewVolumeHandler(dockerClient)

 api := router.Group("/api")
 {
     setupStatusRoutes(api, statusHandler)
     setupContainerRoutes(api, containerHandler, dockerClient)
     setupDockerRoutes(api, dockerHandler, dockerClient)
     setupImageRoutes(api, imageHandler, dockerClient)
     setupStackRoutes(api, stackHandler, dockerClient)
-    setupNetworkRoutes(api, handlers.NewNetworkHandler(dockerClient))
-    setupVolumeRoutes(api, handlers.NewVolumeHandler(dockerClient), dockerClient)
+    setupNetworkRoutes(api, networkHandler, dockerClient)
+    setupVolumeRoutes(api, volumeHandler, dockerClient)
 }
internal/handlers/container_handler.go (1)

171-175: Verify CORS configuration for production use.

The Access-Control-Allow-Origin: * header allows requests from any origin. Ensure this aligns with your security requirements.

Consider making the CORS origin configurable through the agent configuration if you need to restrict access in production environments.

internal/handlers/stack_handler.go (2)

478-495: Docker run to compose conversion is not implemented.

The conversion currently returns a stub response. This functionality appears to be planned for future implementation.

Would you like me to help implement the Docker run to compose conversion logic or create an issue to track this TODO?


555-590: Consider adding unit tests for log parsing logic.

The parseStackLogLine function contains complex parsing logic that would benefit from unit tests to ensure it handles various log formats correctly.

Would you like me to generate unit tests for the log parsing function to cover different log format scenarios?

internal/services/stack.go (1)

586-600: Consider logging process kill failures.

While ignoring Kill() errors is often acceptable, consider logging failures for debugging:

 	case <-ctx.Done():
 		if cmd.Process != nil {
-			cmd.Process.Kill()
+			if err := cmd.Process.Kill(); err != nil {
+				// Process might already be dead, log for debugging
+				fmt.Printf("Failed to kill docker-compose process: %v\n", err)
+			}
 		}
 		return ctx.Err()
 	case err := <-done:
 		if cmd.Process != nil {
-			cmd.Process.Kill()
+			if err := cmd.Process.Kill(); err != nil {
+				// Process might already be dead, log for debugging  
+				fmt.Printf("Failed to kill docker-compose process: %v\n", err)
+			}
 		}
internal/docker/client.go (1)

59-60: Consider making container operation timeouts configurable.

The 10-second timeout for stop/restart operations is hardcoded. Some containers may need more time for graceful shutdown:

Consider adding timeout as a parameter or configuration option:

-func (c *Client) StopContainer(ctx context.Context, containerID string) error {
-	timeout := 10
+func (c *Client) StopContainer(ctx context.Context, containerID string, timeout int) error {
+	if timeout <= 0 {
+		timeout = 10 // default
+	}
 	return c.cli.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout})
 }

Also applies to: 64-65

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2d2755 and 28cf7ee.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (36)
  • .env.example (1 hunks)
  • Dockerfile (1 hunks)
  • cmd/agent/main.go (2 hunks)
  • cmd/agent/main_test.go (0 hunks)
  • go.mod (1 hunks)
  • internal/agent/agent.go (1 hunks)
  • internal/agent/agent_test.go (0 hunks)
  • internal/agent/http_client.go (0 hunks)
  • internal/agent/http_client_test.go (0 hunks)
  • internal/api/router.go (1 hunks)
  • internal/compose/manager.go (0 hunks)
  • internal/compose/manager_test.go (0 hunks)
  • internal/config/config.go (2 hunks)
  • internal/config/config_test.go (0 hunks)
  • internal/docker/client.go (1 hunks)
  • internal/docker/client_test.go (0 hunks)
  • internal/dto/image_dto.go (1 hunks)
  • internal/dto/stack.go (1 hunks)
  • internal/handlers/container_handler.go (1 hunks)
  • internal/handlers/docker_handler.go (1 hunks)
  • internal/handlers/image_handler.go (1 hunks)
  • internal/handlers/network_handler.go (1 hunks)
  • internal/handlers/stack_handler.go (1 hunks)
  • internal/handlers/status_handler.go (1 hunks)
  • internal/handlers/volume_handler.go (1 hunks)
  • internal/middleware/middleware.go (1 hunks)
  • internal/models/stack.go (1 hunks)
  • internal/models/stack_service_info.go (1 hunks)
  • internal/services/container.go (1 hunks)
  • internal/services/stack.go (1 hunks)
  • internal/tasks/docker.go (0 hunks)
  • internal/tasks/manager.go (0 hunks)
  • internal/tasks/manager_test.go (0 hunks)
  • internal/tasks/system.go (0 hunks)
  • pkg/types/message.go (0 hunks)
  • pkg/types/message_test.go (0 hunks)
💤 Files with no reviewable changes (14)
  • cmd/agent/main_test.go
  • pkg/types/message_test.go
  • internal/docker/client_test.go
  • internal/agent/agent_test.go
  • internal/config/config_test.go
  • internal/compose/manager_test.go
  • internal/tasks/docker.go
  • internal/tasks/system.go
  • internal/agent/http_client.go
  • internal/tasks/manager_test.go
  • internal/agent/http_client_test.go
  • internal/compose/manager.go
  • pkg/types/message.go
  • internal/tasks/manager.go
🧰 Additional context used
🪛 golangci-lint (1.64.8)
internal/handlers/volume_handler.go

140-140: Error return value of c.ShouldBindJSON is not checked

(errcheck)

internal/handlers/image_handler.go

183-183: Error return value of c.ShouldBindJSON is not checked

(errcheck)


259-259: Error return value of c.ShouldBindJSON is not checked

(errcheck)

internal/services/stack.go

407-407: Error return value of os.WriteFile is not checked

(errcheck)


589-589: Error return value of cmd.Process.Kill is not checked

(errcheck)


594-594: Error return value of cmd.Process.Kill is not checked

(errcheck)


663-663: Error return value of os.WriteFile is not checked

(errcheck)

🔇 Additional comments (20)
Dockerfile (1)

38-38: Align exposed port with configured default.

EXPOSE 3552 matches the AGENT_PORT default (3552) in .env.example and internal/config/config.go, ensuring consistency between the Docker image and application configuration.

.env.example (1)

1-4: Provide environment variable template for agent configuration.

The .env.example file lists all required settings (AGENT_ID, AGENT_LISTEN_ADDRESS, AGENT_PORT, API_KEY) that align with config.Load. It serves as a clear reference for users to configure the agent.

cmd/agent/main.go (2)

35-35: Graceful shutdown invocation is handled correctly.

Calling a.Stop() on receiving shutdown signals appropriately triggers the agent's teardown process.


39-41: Handle startup errors for the agent.

Using log.Fatalf on a.Start() failure properly surfaces critical initialization issues.

internal/dto/image_dto.go (1)

1-5: Define ImagePullDto for image pull requests.

The DTO struct includes the required ImageName field with JSON binding, aligning with Gin's request validation patterns.

go.mod (2)

3-3: Verify the Go version validity.

Go 1.24.3 appears to be an unusually high version number. As of my knowledge cutoff, the latest stable Go version was around 1.22.x. Please verify that this version exists and is stable.

What is the latest stable version of Go programming language?

5-74: Verify security advisories for new dependencies.

The major dependency additions suggest a significant architectural refactor to REST API with Docker SDK integration. Ensure all dependencies are secure and up-to-date.

#!/bin/bash
# Description: Check for security advisories for major new dependencies

echo "Checking security advisories for major dependencies..."

# Check gin-gonic/gin
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: GO, package: "github.com/gin-gonic/gin") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'

echo "---"

# Check docker/docker 
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: GO, package: "github.com/docker/docker") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'
internal/handlers/status_handler.go (1)

1-29: Clean implementation with good practices.

The status handler follows excellent patterns:

  • Constructor-based dependency injection
  • Consistent JSON response structure with data, success fields
  • Proper HTTP status codes
  • Clean separation of concerns

The implementation is straightforward and correct.

internal/handlers/docker_handler.go (1)

1-35: Well-implemented Docker info handler.

Excellent implementation demonstrating:

  • Proper error handling with appropriate HTTP status codes (500 for server errors)
  • Context propagation from HTTP request to Docker API calls
  • Consistent JSON response structure matching other handlers
  • Clean dependency injection pattern

The use of c.Request.Context() ensures proper cancellation and timeout handling.

internal/middleware/middleware.go (1)

25-37: Good Docker availability validation.

The Docker availability middleware properly validates the client and returns appropriate HTTP 503 status. The implementation correctly handles the nil check and provides clear error messaging.

internal/dto/stack.go (1)

1-25: Well-structured DTOs for stack operations.

The DTOs demonstrate excellent design principles:

  • Proper use of validation tags (binding:"required") for mandatory fields
  • Appropriate pointer usage for optional fields in update operations
  • Consistent JSON field naming (camelCase)
  • Clear separation of concerns across different stack operations (Create, Update, Redeploy, Destroy)
  • Comprehensive coverage of stack lifecycle operations

The structure supports flexible API operations while maintaining type safety.

internal/handlers/network_handler.go (1)

1-232: Well-structured network handlers

The network handler implementation is clean and follows consistent patterns for error handling and response formatting.

internal/handlers/image_handler.go (1)

100-130: Good streaming implementation

The streaming implementation for image pull properly handles response headers, error conditions, and flushing. Well done!

internal/agent/agent.go (3)

122-133: LGTM! Thread-safe status management.

The status management implementation correctly uses mutex for thread-safe access.


93-106: Well-implemented graceful shutdown.

The graceful shutdown properly handles the API server shutdown with timeout and closes the Docker client when present.


34-38: ```shell
#!/bin/bash

Show router setup for all resource groups including networks

sed -n '1,100p' internal/api/router.go

Inspect setupNetworkRoutes definition to see middleware usage

sed -n '100,200p' internal/api/router.go


</details>
<details>
<summary>internal/api/router.go (1)</summary>

`86-109`: **Well-organized stack routes.**

The stack routes provide comprehensive operations with proper RESTful design and middleware protection.

</details>
<details>
<summary>internal/handlers/container_handler.go (1)</summary>

`13-206`: **Well-implemented container handler.**

The container handler provides a clean API with consistent error handling, proper context propagation, and well-structured SSE streaming for real-time stats.

</details>
<details>
<summary>internal/models/stack.go (1)</summary>

`1-72`: **Well-structured model definitions.**

The models provide comprehensive representations of stack components with appropriate use of pointers for optional fields and proper JSON serialization tags.

</details>
<details>
<summary>internal/handlers/stack_handler.go (1)</summary>

`16-553`: **Comprehensive stack management implementation.**

The stack handler provides a complete set of operations with proper error handling and real-time log streaming via SSE. The implementation is well-structured and follows consistent patterns.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +3 to +9
type StackServiceInfo struct {
Name string `json:"name"`
Image string `json:"image"`
Status string `json:"status"`
ContainerID string `json:"container_id"`
Ports []string `json:"ports"`
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Inconsistent JSON field naming conventions.

The JSON tag container_id uses snake_case, whereas DTOs like ImagePullDto use camelCase (imageName). Align all JSON tags to camelCase (e.g., containerId) to maintain a consistent API contract.

🤖 Prompt for AI Agents
In internal/models/stack_service_info.go lines 3 to 9, the JSON tag for
ContainerID uses snake_case ("container_id") which is inconsistent with other
DTOs that use camelCase. Change the JSON tag from `container_id` to
`containerId` to align with the camelCase naming convention used across the API.

Comment on lines +10 to +23
func APIKeyMiddleware(expectedAPIKey string) gin.HandlerFunc {
return func(c *gin.Context) {
apiKey := c.GetHeader("X-API-Key")
if apiKey != expectedAPIKey {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"data": nil,
"success": false,
"error": "Unauthorized",
})
return
}
c.Next()
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using constant-time comparison for API key validation.

The API key comparison on line 13 could be vulnerable to timing attacks. Consider using crypto/subtle.ConstantTimeCompare for secure string comparison.

+import (
+	"crypto/subtle"
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+	"github.com/ofkm/arcane-agent/internal/docker"
+)

 func APIKeyMiddleware(expectedAPIKey string) gin.HandlerFunc {
 	return func(c *gin.Context) {
 		apiKey := c.GetHeader("X-API-Key")
-		if apiKey != expectedAPIKey {
+		if subtle.ConstantTimeCompare([]byte(apiKey), []byte(expectedAPIKey)) != 1 {
 			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In internal/middleware/middleware.go around lines 10 to 23, the API key
comparison uses a simple equality check which can be vulnerable to timing
attacks. Replace the direct string comparison with
crypto/subtle.ConstantTimeCompare by converting both the expectedAPIKey and the
received apiKey to byte slices and comparing them securely. This change will
ensure the comparison runs in constant time, improving security.

Comment on lines +22 to +36
func (s *ContainerService) GetStats(ctx context.Context, containerID string, stream bool) (interface{}, error) {
stats, err := s.dockerClient.ContainerStats(ctx, containerID, stream)
if err != nil {
return nil, fmt.Errorf("failed to get container stats: %w", err)
}
defer stats.Body.Close()

var statsData interface{}
decoder := json.NewDecoder(stats.Body)
if err := decoder.Decode(&statsData); err != nil {
return nil, fmt.Errorf("failed to decode stats: %w", err)
}

return statsData, nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix streaming behavior in GetStats method

The method accepts a stream parameter but only reads a single stats object regardless of its value. When stream=true, the caller might expect continuous stats but receives only one reading.

Consider either:

  1. Remove the stream parameter if single stats reading is the intended behavior
  2. Implement proper streaming logic similar to StreamStats
 func (s *ContainerService) GetStats(ctx context.Context, containerID string, stream bool) (interface{}, error) {
+	// For streaming, use StreamStats method instead
+	if stream {
+		return nil, fmt.Errorf("use StreamStats method for streaming stats")
+	}
 	stats, err := s.dockerClient.ContainerStats(ctx, containerID, stream)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *ContainerService) GetStats(ctx context.Context, containerID string, stream bool) (interface{}, error) {
stats, err := s.dockerClient.ContainerStats(ctx, containerID, stream)
if err != nil {
return nil, fmt.Errorf("failed to get container stats: %w", err)
}
defer stats.Body.Close()
var statsData interface{}
decoder := json.NewDecoder(stats.Body)
if err := decoder.Decode(&statsData); err != nil {
return nil, fmt.Errorf("failed to decode stats: %w", err)
}
return statsData, nil
}
func (s *ContainerService) GetStats(ctx context.Context, containerID string, stream bool) (interface{}, error) {
// For streaming, use StreamStats method instead
if stream {
return nil, fmt.Errorf("use StreamStats method for streaming stats")
}
stats, err := s.dockerClient.ContainerStats(ctx, containerID, stream)
if err != nil {
return nil, fmt.Errorf("failed to get container stats: %w", err)
}
defer stats.Body.Close()
var statsData interface{}
decoder := json.NewDecoder(stats.Body)
if err := decoder.Decode(&statsData); err != nil {
return nil, fmt.Errorf("failed to decode stats: %w", err)
}
return statsData, nil
}
🤖 Prompt for AI Agents
In internal/services/container.go around lines 22 to 36, the GetStats method
accepts a stream parameter but always reads and returns only a single stats
object, which is incorrect when stream is true. To fix this, either remove the
stream parameter if only a single stats reading is intended, or implement proper
streaming logic that continuously reads and processes stats data when stream is
true, similar to the StreamStats method.

Comment on lines +169 to +191
// Try to bind JSON body for filters (optional)
c.ShouldBindJSON(&req)

// Create filter args
filterArgs := filters.NewArgs()
if req.Filters != nil {
for key, values := range req.Filters {
for _, value := range values {
filterArgs.Add(key, value)
}
}
}

var response volume.PruneReport
var err error

if len(filterArgs.Get("")) > 0 {
// Use the method with custom filters if available
response, err = h.dockerClient.PruneVolumesWithFilters(c.Request.Context(), filterArgs)
} else {
// Use the basic method
response, err = h.dockerClient.PruneVolumes(c.Request.Context())
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix filter checking logic and handle binding error

Two issues here:

  1. The error from ShouldBindJSON is not checked
  2. Line 185 checks len(filterArgs.Get("")) which always returns 0
 // Try to bind JSON body for filters (optional)
-c.ShouldBindJSON(&req)
+_ = c.ShouldBindJSON(&req) // Ignore error as JSON body is optional

 // Create filter args
 filterArgs := filters.NewArgs()
 if req.Filters != nil {
 	for key, values := range req.Filters {
 		for _, value := range values {
 			filterArgs.Add(key, value)
 		}
 	}
 }

 var response volume.PruneReport
 var err error

-if len(filterArgs.Get("")) > 0 {
+if filterArgs.Len() > 0 {
 	// Use the method with custom filters if available
 	response, err = h.dockerClient.PruneVolumesWithFilters(c.Request.Context(), filterArgs)
 } else {
 	// Use the basic method
 	response, err = h.dockerClient.PruneVolumes(c.Request.Context())
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Try to bind JSON body for filters (optional)
c.ShouldBindJSON(&req)
// Create filter args
filterArgs := filters.NewArgs()
if req.Filters != nil {
for key, values := range req.Filters {
for _, value := range values {
filterArgs.Add(key, value)
}
}
}
var response volume.PruneReport
var err error
if len(filterArgs.Get("")) > 0 {
// Use the method with custom filters if available
response, err = h.dockerClient.PruneVolumesWithFilters(c.Request.Context(), filterArgs)
} else {
// Use the basic method
response, err = h.dockerClient.PruneVolumes(c.Request.Context())
}
// Try to bind JSON body for filters (optional)
_ = c.ShouldBindJSON(&req) // Ignore error as JSON body is optional
// Create filter args
filterArgs := filters.NewArgs()
if req.Filters != nil {
for key, values := range req.Filters {
for _, value := range values {
filterArgs.Add(key, value)
}
}
}
var response volume.PruneReport
var err error
if filterArgs.Len() > 0 {
// Use the method with custom filters if available
response, err = h.dockerClient.PruneVolumesWithFilters(c.Request.Context(), filterArgs)
} else {
// Use the basic method
response, err = h.dockerClient.PruneVolumes(c.Request.Context())
}
🤖 Prompt for AI Agents
In internal/handlers/volume_handler.go around lines 169 to 191, first capture
and check the error returned by c.ShouldBindJSON to handle JSON binding failures
properly. Second, replace the incorrect check len(filterArgs.Get("")) with a
proper check to determine if any filters exist, such as checking if
filterArgs.Len() > 0 or another appropriate method to verify if filters were
added. This ensures correct logic for choosing between PruneVolumesWithFilters
and PruneVolumes.

Comment on lines +139 to +143
// Check for force parameter in query or body
c.ShouldBindJSON(&req)
if c.Query("force") == "true" {
req.Force = true
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Check error return from ShouldBindJSON

The error from ShouldBindJSON is not checked, which could lead to unexpected behavior.

 // Check for force parameter in query or body
-c.ShouldBindJSON(&req)
+if err := c.ShouldBindJSON(&req); err != nil && c.ContentType() == "application/json" {
+	// Only treat as error if content-type is JSON
+	c.JSON(http.StatusBadRequest, gin.H{
+		"data":    nil,
+		"success": false,
+		"error":   err.Error(),
+	})
+	return
+}
 if c.Query("force") == "true" {
 	req.Force = true
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check for force parameter in query or body
c.ShouldBindJSON(&req)
if c.Query("force") == "true" {
req.Force = true
}
// Check for force parameter in query or body
if err := c.ShouldBindJSON(&req); err != nil && c.ContentType() == "application/json" {
// Only treat as error if content-type is JSON
c.JSON(http.StatusBadRequest, gin.H{
"data": nil,
"success": false,
"error": err.Error(),
})
return
}
if c.Query("force") == "true" {
req.Force = true
}
🧰 Tools
🪛 golangci-lint (1.64.8)

140-140: Error return value of c.ShouldBindJSON is not checked

(errcheck)

🤖 Prompt for AI Agents
In internal/handlers/volume_handler.go around lines 139 to 143, the error
returned by ShouldBindJSON is not checked, which can cause unexpected behavior
if JSON binding fails. Modify the code to capture the error returned by
ShouldBindJSON and handle it appropriately, such as returning a bad request
response or logging the error before proceeding.

Comment on lines +266 to +267
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect timestamp initialization.

Setting CreatedAt and UpdatedAt to time.Now() for existing stacks is incorrect. These should only be set as defaults when metadata is missing:

 			ServiceCount: 0,
 			RunningCount: 0,
-			CreatedAt:    time.Now(),
-			UpdatedAt:    time.Now(),
+			CreatedAt:    time.Time{}, // Will be set from metadata or file info
+			UpdatedAt:    time.Time{},

Consider using file modification time as a fallback when metadata is missing.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
ServiceCount: 0,
RunningCount: 0,
CreatedAt: time.Time{}, // Will be set from metadata or file info
UpdatedAt: time.Time{},
🤖 Prompt for AI Agents
In internal/services/stack.go around lines 266 to 267, the code incorrectly sets
CreatedAt and UpdatedAt to time.Now() for existing stacks. Instead, modify the
logic to check if metadata timestamps are present; if missing, use the file's
modification time as a fallback to initialize CreatedAt and UpdatedAt. This
ensures timestamps reflect actual creation or update times rather than the
current time.

Comment on lines +488 to +525
func (s *StackService) GetStackServices(ctx context.Context, stackID string) ([]models.StackServiceInfo, error) {
stack, err := s.GetStackByID(ctx, stackID)
if err != nil {
return nil, err
}

cmd := exec.CommandContext(ctx, "docker-compose", "ps", "--format", "json")
cmd.Dir = stack.Path
cmd.Env = append(os.Environ(),
fmt.Sprintf("COMPOSE_PROJECT_NAME=%s", stack.Name),
)

var services []models.StackServiceInfo

output, err := cmd.Output()
if err == nil {
services, err = s.parseComposePS(string(output))
if err != nil {
return nil, fmt.Errorf("failed to parse compose ps output: %w", err)
}
}

if len(services) > 0 {
return services, nil
}

composeFile := s.findComposeFile(stack.Path)
if composeFile == "" {
return []models.StackServiceInfo{}, nil
}

servicesFromFile, err := s.parseServicesFromComposeFile(composeFile, stack.Name)
if err != nil {
return []models.StackServiceInfo{}, nil
}

return servicesFromFile, nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Eliminate code duplication in GetStackServices.

The method duplicates logic from getStackServicesDirectly. Refactor to reuse the existing method:

 func (s *StackService) GetStackServices(ctx context.Context, stackID string) ([]models.StackServiceInfo, error) {
 	stack, err := s.GetStackByID(ctx, stackID)
 	if err != nil {
 		return nil, err
 	}
-
-	cmd := exec.CommandContext(ctx, "docker-compose", "ps", "--format", "json")
-	cmd.Dir = stack.Path
-	cmd.Env = append(os.Environ(),
-		fmt.Sprintf("COMPOSE_PROJECT_NAME=%s", stack.Name),
-	)
-
-	var services []models.StackServiceInfo
-
-	output, err := cmd.Output()
-	if err == nil {
-		services, err = s.parseComposePS(string(output))
-		if err != nil {
-			return nil, fmt.Errorf("failed to parse compose ps output: %w", err)
-		}
-	}
-
-	if len(services) > 0 {
-		return services, nil
-	}
-
-	composeFile := s.findComposeFile(stack.Path)
-	if composeFile == "" {
-		return []models.StackServiceInfo{}, nil
-	}
-
-	servicesFromFile, err := s.parseServicesFromComposeFile(composeFile, stack.Name)
-	if err != nil {
-		return []models.StackServiceInfo{}, nil
-	}
-
-	return servicesFromFile, nil
+	return s.getStackServicesDirectly(ctx, stack)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *StackService) GetStackServices(ctx context.Context, stackID string) ([]models.StackServiceInfo, error) {
stack, err := s.GetStackByID(ctx, stackID)
if err != nil {
return nil, err
}
cmd := exec.CommandContext(ctx, "docker-compose", "ps", "--format", "json")
cmd.Dir = stack.Path
cmd.Env = append(os.Environ(),
fmt.Sprintf("COMPOSE_PROJECT_NAME=%s", stack.Name),
)
var services []models.StackServiceInfo
output, err := cmd.Output()
if err == nil {
services, err = s.parseComposePS(string(output))
if err != nil {
return nil, fmt.Errorf("failed to parse compose ps output: %w", err)
}
}
if len(services) > 0 {
return services, nil
}
composeFile := s.findComposeFile(stack.Path)
if composeFile == "" {
return []models.StackServiceInfo{}, nil
}
servicesFromFile, err := s.parseServicesFromComposeFile(composeFile, stack.Name)
if err != nil {
return []models.StackServiceInfo{}, nil
}
return servicesFromFile, nil
}
func (s *StackService) GetStackServices(ctx context.Context, stackID string) ([]models.StackServiceInfo, error) {
stack, err := s.GetStackByID(ctx, stackID)
if err != nil {
return nil, err
}
return s.getStackServicesDirectly(ctx, stack)
}
🤖 Prompt for AI Agents
In internal/services/stack.go around lines 488 to 525, the GetStackServices
method duplicates logic already implemented in getStackServicesDirectly.
Refactor GetStackServices to call getStackServicesDirectly instead of repeating
the same code. This will eliminate duplication and improve maintainability by
reusing the existing method for fetching stack services.

Comment on lines +152 to +157
func (c *Client) BuildImage(ctx context.Context, contextPath string, dockerfile string, tags []string, buildArgs map[string]string, target string, platform string) (string, error) {
// This is a simplified implementation
// In a real implementation, you'd need to create a tar archive of the build context
// and handle the build response stream properly
return "", fmt.Errorf("build image not implemented yet")
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implement or remove the BuildImage method.

The method returns a "not implemented" error, which could cause runtime failures. Either implement it properly or remove it until needed:

Would you like me to help implement the BuildImage method using the Docker SDK's build functionality?

🤖 Prompt for AI Agents
In internal/docker/client.go around lines 152 to 157, the BuildImage method
currently returns a "not implemented" error, which can cause runtime failures.
To fix this, either fully implement the BuildImage method using the Docker SDK's
build functionality, including creating a tar archive of the build context and
handling the build response stream, or remove the method entirely until it is
needed to avoid runtime errors.

Comment on lines +90 to 113
func (c *Client) PullImage(ctx context.Context, fromImage string, tag string, platform string) error {
pullOptions := image.PullOptions{
Platform: platform,
}

return map[string]interface{}{
"images": images,
}, nil
}
imageRef := fromImage
if tag != "" {
imageRef = fmt.Sprintf("%s:%s", fromImage, tag)
}

// GetSystemInfo gets Docker system information
func (c *Client) GetSystemInfo(ctx context.Context) (interface{}, error) {
output, err := c.ExecuteCommand("system", []string{"info", "--format", "json"})
reader, err := c.cli.ImagePull(ctx, imageRef, pullOptions)
if err != nil {
return nil, err
return fmt.Errorf("failed to pull image: %w", err)
}
defer reader.Close()

var systemInfo map[string]interface{}
if err := json.Unmarshal([]byte(output), &systemInfo); err != nil {
// If JSON parsing fails, return raw output
return map[string]interface{}{
"system_info": output,
}, nil
// Read the response to ensure the pull completes
_, err = io.ReadAll(reader)
if err != nil {
return fmt.Errorf("failed to read pull response: %w", err)
}

return systemInfo, nil
return nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add authentication support for private registries.

The pull and push operations don't support authentication, which will fail with private registries:

Consider adding authentication support:

import "github.com/docker/docker/api/types/registry"

func (c *Client) PullImageWithAuth(ctx context.Context, fromImage string, tag string, platform string, authConfig registry.AuthConfig) error {
    pullOptions := image.PullOptions{
        Platform: platform,
        RegistryAuth: encodeAuthConfig(authConfig), // You'll need to implement auth encoding
    }
    // ... rest of the implementation
}

This allows pulling from private registries with proper credentials.

Also applies to: 168-189

🤖 Prompt for AI Agents
In internal/docker/client.go around lines 90 to 113, the PullImage function
lacks support for authentication, which causes failures when pulling images from
private registries. To fix this, add a new method (e.g., PullImageWithAuth) that
accepts an additional authConfig parameter of type registry.AuthConfig.
Implement encoding of this authConfig into a base64 string and set it in the
PullOptions' RegistryAuth field before calling ImagePull. Apply a similar change
to the push operation around lines 168 to 189 to support authentication there as
well.

Comment on lines +22 to 32
func NewClient() (*Client, error) {
cli, err := client.NewClientWithOpts(
client.WithHost("unix:///var/run/docker.sock"),
client.WithAPIVersionNegotiation(),
)
if err != nil {
return "", fmt.Errorf("docker %s failed: %s", command, string(output))
return nil, fmt.Errorf("failed to create Docker client: %w", err)
}

return strings.TrimSpace(string(output)), nil
return &Client{cli: cli}, nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make Docker endpoint configurable for better portability.

The Unix socket path is hardcoded, which won't work on Windows or with remote Docker hosts. Consider making it configurable:

-func NewClient() (*Client, error) {
+func NewClient(dockerHost string) (*Client, error) {
+	opts := []client.Opt{
+		client.WithAPIVersionNegotiation(),
+	}
+	
+	if dockerHost != "" {
+		opts = append(opts, client.WithHost(dockerHost))
+	} else {
+		// Use default from environment or Docker's default
+		opts = append(opts, client.FromEnv)
+	}
+	
 	cli, err := client.NewClientWithOpts(
-		client.WithHost("unix:///var/run/docker.sock"),
-		client.WithAPIVersionNegotiation(),
+		opts...,
 	)

This allows using DOCKER_HOST environment variable or explicit configuration.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func NewClient() (*Client, error) {
cli, err := client.NewClientWithOpts(
client.WithHost("unix:///var/run/docker.sock"),
client.WithAPIVersionNegotiation(),
)
if err != nil {
return "", fmt.Errorf("docker %s failed: %s", command, string(output))
return nil, fmt.Errorf("failed to create Docker client: %w", err)
}
return strings.TrimSpace(string(output)), nil
return &Client{cli: cli}, nil
}
func NewClient(dockerHost string) (*Client, error) {
opts := []client.Opt{
client.WithAPIVersionNegotiation(),
}
if dockerHost != "" {
opts = append(opts, client.WithHost(dockerHost))
} else {
// Use default from environment or Docker's default
opts = append(opts, client.FromEnv)
}
cli, err := client.NewClientWithOpts(
opts...,
)
if err != nil {
return nil, fmt.Errorf("failed to create Docker client: %w", err)
}
return &Client{cli: cli}, nil
}
🤖 Prompt for AI Agents
In internal/docker/client.go around lines 22 to 32, the Docker client is created
with a hardcoded Unix socket path, limiting portability. Modify the code to read
the Docker host endpoint from the DOCKER_HOST environment variable or accept it
as a configuration parameter, falling back to the default Unix socket if not
set. This change will allow the client to connect to Docker on different
platforms or remote hosts.

@kmendell kmendell added this to the v2.0.0 milestone Jun 30, 2025
@kmendell kmendell closed this Jul 28, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants