Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions internal/cloud/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,13 @@ func TestHandleRequest_CallTool_RestartContainer(t *testing.T) {
mockClient := &MockClientService{}
mockClient.On("ContainerAction", mock.Anything, mock.Anything, container.Restart).Return(nil)

cs := container_support.NewContainerService(mockClient, container.Container{ID: "abc123"})
cs := container_support.NewContainerService(mockClient, container.Container{ID: "abc123", Host: "local"})

mockHost := &MockHostService{}
mockHost.On("ListAllContainers", container.ContainerLabels(nil)).Return([]container.Container{
{ID: "abc123", Name: "nginx", Host: "local", State: "running"},
}, nil)
mockHost.On("Hosts").Return([]container.Host{{ID: "local", Name: "my-server"}})
mockHost.On("FindContainer", "local", "abc123", container.ContainerLabels(nil)).Return(cs, nil)

client := &Client{
Expand All @@ -143,7 +147,7 @@ func TestHandleRequest_CallTool_RestartContainer(t *testing.T) {
Type: &pb.ToolRequest_CallTool{
CallTool: &pb.CallToolRequest{
Name: "restart_container",
ArgumentsJson: `{"container_id": "abc123", "host_id": "local"}`,
ArgumentsJson: `{"container_id": "abc123"}`,
},
},
}
Expand Down
19 changes: 9 additions & 10 deletions internal/cloud/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func AvailableTools(enableActions bool) []*pb.ToolDefinition {
},
{
Name: "find_containers",
Description: "Search for Docker containers by name, state, or health status. All parameters are optional. Returns container ID, name, image, state, health, and host. Use this before start/stop/restart actions to get the container ID and host.",
Description: "Search for Docker containers by name, state, or health status. All parameters are optional. Returns container ID, name, image, state, health, and host. Use this before other container tools to get the container ID.",
ParametersJson: findContainerParams,
},
{
Expand All @@ -51,40 +51,39 @@ func AvailableTools(enableActions bool) []*pb.ToolDefinition {
},
{
Name: "fetch_container_logs",
Description: "Fetch raw logs from a running Docker container. Requires container_id and host from find_containers. Optionally filter by time range, log level, text search, or regex pattern. Returns up to 100 matching log lines.",
ParametersJson: `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID (from find_containers)"},"host_id":{"type":"string","description":"The host ID where the container is running (from find_containers)"},"start":{"type":"string","description":"Optional ISO 8601 start time for log range"},"end":{"type":"string","description":"Optional ISO 8601 end time for log range"},"level":{"type":"string","description":"Optional log level filter (e.g. error, warn, info)"},"query":{"type":"string","description":"Optional text search query (case-insensitive substring match)"},"regex":{"type":"string","description":"Optional regex pattern to match against log messages"}},"required":["container_id","host_id"]}`,
Description: "Fetch raw logs from a Docker container. Requires container_id from find_containers. Optionally filter by time range, log level, text search, or regex pattern. Returns up to 100 matching log lines.",
ParametersJson: `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID (from find_containers)"},"start":{"type":"string","description":"Optional ISO 8601 start time for log range"},"end":{"type":"string","description":"Optional ISO 8601 end time for log range"},"level":{"type":"string","description":"Optional log level filter (e.g. error, warn, info)"},"query":{"type":"string","description":"Optional text search query (case-insensitive substring match)"},"regex":{"type":"string","description":"Optional regex pattern to match against log messages"}},"required":["container_id"]}`,
},
}

inspectParams := `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID (from find_containers)"},"host_id":{"type":"string","description":"The host ID where the container is running (from find_containers)"}},"required":["container_id","host_id"]}`
containerIDParams := `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID (from find_containers)"}},"required":["container_id"]}`
tools = append(tools, &pb.ToolDefinition{
Name: "inspect_container",
Description: "Get detailed configuration of a Docker container including environment variables, port mappings, mounts, restart policy, network mode, labels, and resource limits.",
ParametersJson: inspectParams,
ParametersJson: containerIDParams,
})

if enableActions {
actionParams := `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID (from find_containers)"},"host_id":{"type":"string","description":"The host ID where the container is running (from find_containers)"}},"required":["container_id","host_id"]}`
tools = append(tools,
&pb.ToolDefinition{
Name: "start_container",
Description: "Start a stopped Docker container",
ParametersJson: actionParams,
ParametersJson: containerIDParams,
},
&pb.ToolDefinition{
Name: "stop_container",
Description: "Stop a running Docker container",
ParametersJson: actionParams,
ParametersJson: containerIDParams,
},
&pb.ToolDefinition{
Name: "restart_container",
Description: "Restart a Docker container",
ParametersJson: actionParams,
ParametersJson: containerIDParams,
},
&pb.ToolDefinition{
Name: "update_container",
Description: "Update a Docker container by pulling the latest version of its image and recreating it with the same configuration. If the image is already up to date, no recreation occurs. For swarm service containers, updates the service instead.",
ParametersJson: actionParams,
ParametersJson: containerIDParams,
},
)
}
Expand Down
15 changes: 4 additions & 11 deletions internal/cloud/tools_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (

type containerActionArgs struct {
ContainerID string `json:"container_id"`
Host string `json:"host_id"`
}

func executeContainerAction(ctx context.Context, name string, argsJSON string, hostService ToolHostService, labels container.ContainerLabels) (*pb.CallToolResponse, error) {
Expand All @@ -28,13 +27,10 @@ func executeContainerAction(ctx context.Context, name string, argsJSON string, h
if args.ContainerID == "" {
return nil, fmt.Errorf("container_id is required")
}
if args.Host == "" {
return nil, fmt.Errorf("host is required")
}

cs, err := hostService.FindContainer(args.Host, args.ContainerID, labels)
cs, err := findContainerByID(args.ContainerID, hostService, labels)
if err != nil {
return nil, fmt.Errorf("container not found: %w", err)
return nil, err
}

if err := cs.Action(ctx, action); err != nil {
Expand All @@ -60,13 +56,10 @@ func executeUpdateContainer(ctx context.Context, argsJSON string, hostService To
if args.ContainerID == "" {
return nil, fmt.Errorf("container_id is required")
}
if args.Host == "" {
return nil, fmt.Errorf("host is required")
}

cs, err := hostService.FindContainer(args.Host, args.ContainerID, labels)
cs, err := findContainerByID(args.ContainerID, hostService, labels)
if err != nil {
return nil, fmt.Errorf("container not found: %w", err)
return nil, err
}

progressCh := make(chan container.UpdateProgress, 100)
Expand Down
9 changes: 4 additions & 5 deletions internal/cloud/tools_containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (

type inspectContainerArgs struct {
ContainerID string `json:"container_id"`
Host string `json:"host_id"`
}

type findContainersArgs struct {
Expand Down Expand Up @@ -156,13 +155,13 @@ func executeInspectContainer(argsJSON string, hostService ToolHostService, label
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
return nil, fmt.Errorf("failed to parse arguments: %w", err)
}
if args.ContainerID == "" || args.Host == "" {
return nil, fmt.Errorf("container_id and host are required")
if args.ContainerID == "" {
return nil, fmt.Errorf("container_id is required")
}

cs, err := hostService.FindContainer(args.Host, args.ContainerID, labels)
cs, err := findContainerByID(args.ContainerID, hostService, labels)
if err != nil {
return nil, fmt.Errorf("container not found: %w", err)
return nil, err
}

c := cs.Container
Expand Down
15 changes: 15 additions & 0 deletions internal/cloud/tools_helpers.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package cloud

import (
"fmt"
"strings"
"time"

"github.com/amir20/dozzle/internal/container"
container_support "github.com/amir20/dozzle/internal/support/container"
pb "github.com/amir20/dozzle/proto/cloud"
"github.com/rs/zerolog/log"
)
Expand Down Expand Up @@ -62,3 +64,16 @@ func logHostErrors(errs []error) {
}
}
}

// findContainerByID searches across all hosts to find a container by ID and returns its ContainerService.
func findContainerByID(containerID string, hostService ToolHostService, labels container.ContainerLabels) (*container_support.ContainerService, error) {
containers, errs := hostService.ListAllContainers(labels)
logHostErrors(errs)

for _, c := range containers {
if c.ID == containerID {
return hostService.FindContainer(c.Host, c.ID, labels)
}
}
return nil, fmt.Errorf("container %s not found on any host", containerID)
}
9 changes: 4 additions & 5 deletions internal/cloud/tools_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (

type fetchLogsArgs struct {
ContainerID string `json:"container_id"`
Host string `json:"host_id"`
Start string `json:"start"`
End string `json:"end"`
Level string `json:"level"`
Expand All @@ -27,13 +26,13 @@ func executeFetchContainerLogs(ctx context.Context, argsJSON string, hostService
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
return nil, fmt.Errorf("failed to parse arguments: %w", err)
}
if args.ContainerID == "" || args.Host == "" {
return nil, fmt.Errorf("container_id and host are required")
if args.ContainerID == "" {
return nil, fmt.Errorf("container_id is required")
}

cs, err := hostService.FindContainer(args.Host, args.ContainerID, labels)
cs, err := findContainerByID(args.ContainerID, hostService, labels)
if err != nil {
return nil, fmt.Errorf("container not found: %w", err)
return nil, err
}

start := time.Now().Add(-1 * time.Hour)
Expand Down
8 changes: 6 additions & 2 deletions internal/cloud/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,16 @@ func TestExecuteTool_RestartContainer(t *testing.T) {
mockClient := &MockClientService{}
mockClient.On("ContainerAction", mock.Anything, mock.Anything, container.Restart).Return(nil)

cs := container_support.NewContainerService(mockClient, container.Container{ID: "abc123"})
cs := container_support.NewContainerService(mockClient, container.Container{ID: "abc123", Host: "local"})

mockHost := &MockHostService{}
mockHost.On("ListAllContainers", container.ContainerLabels(nil)).Return([]container.Container{
{ID: "abc123", Name: "nginx", Host: "local", State: "running"},
}, nil)
mockHost.On("Hosts").Return([]container.Host{{ID: "local", Name: "my-server"}})
mockHost.On("FindContainer", "local", "abc123", container.ContainerLabels(nil)).Return(cs, nil)

argsJSON := `{"container_id": "abc123", "host_id": "local"}`
argsJSON := `{"container_id": "abc123"}`
resp := ExecuteTool(context.Background(), "restart_container", argsJSON, true, mockHost, nil)
assert.True(t, resp.Success)

Expand Down
Loading