diff --git a/internal/cloud/client_test.go b/internal/cloud/client_test.go index 34620913..cc8ce4f3 100644 --- a/internal/cloud/client_test.go +++ b/internal/cloud/client_test.go @@ -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{ @@ -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"}`, }, }, } diff --git a/internal/cloud/tools.go b/internal/cloud/tools.go index bc363e25..17dca6bb 100644 --- a/internal/cloud/tools.go +++ b/internal/cloud/tools.go @@ -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, }, { @@ -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, }, ) } diff --git a/internal/cloud/tools_actions.go b/internal/cloud/tools_actions.go index ff69578b..79888247 100644 --- a/internal/cloud/tools_actions.go +++ b/internal/cloud/tools_actions.go @@ -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) { @@ -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 { @@ -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) diff --git a/internal/cloud/tools_containers.go b/internal/cloud/tools_containers.go index 6b505776..86072c8e 100644 --- a/internal/cloud/tools_containers.go +++ b/internal/cloud/tools_containers.go @@ -12,7 +12,6 @@ import ( type inspectContainerArgs struct { ContainerID string `json:"container_id"` - Host string `json:"host_id"` } type findContainersArgs struct { @@ -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 diff --git a/internal/cloud/tools_helpers.go b/internal/cloud/tools_helpers.go index 6f1b67ca..e9a5c4d1 100644 --- a/internal/cloud/tools_helpers.go +++ b/internal/cloud/tools_helpers.go @@ -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" ) @@ -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) +} diff --git a/internal/cloud/tools_logs.go b/internal/cloud/tools_logs.go index c3b00320..0268cb5f 100644 --- a/internal/cloud/tools_logs.go +++ b/internal/cloud/tools_logs.go @@ -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"` @@ -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) diff --git a/internal/cloud/tools_test.go b/internal/cloud/tools_test.go index e6913e64..b9e1ddb8 100644 --- a/internal/cloud/tools_test.go +++ b/internal/cloud/tools_test.go @@ -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)