mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 10:54:44 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a058dc287a | |||
| 7ac04a0715 |
@@ -50,12 +50,12 @@ 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 running 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 returned in the 'id' field from find_containers or list_running_containers. This is a Docker container ID (e.g. 'abc123def456'), NOT the container name."},"host_id":{"type":"string","description":"Optional. The host ID returned in the 'host_id' field from find_containers. This is a host identifier (e.g. 'localhost' or a hash), NOT the host display name. If omitted, all hosts are searched."},"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"]}`
|
||||
inspectParams := `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID returned in the 'id' field from find_containers or list_running_containers. This is a Docker container ID (e.g. 'abc123def456'), NOT the container name."},"host_id":{"type":"string","description":"Optional. The host ID returned in the 'host_id' field from find_containers. This is a host identifier (e.g. 'localhost' or a hash), NOT the host display name. If omitted, all hosts are searched."}},"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.",
|
||||
@@ -63,7 +63,7 @@ func AvailableTools(enableActions bool) []*pb.ToolDefinition {
|
||||
})
|
||||
|
||||
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"]}`
|
||||
actionParams := `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID returned in the 'id' field from find_containers. This is a Docker container ID, NOT the container name."},"host_id":{"type":"string","description":"Optional. The host ID returned in the 'host_id' field from find_containers. This is a host identifier, NOT the host display name. If omitted, all hosts are searched."}},"required":["container_id"]}`
|
||||
tools = append(tools,
|
||||
&pb.ToolDefinition{
|
||||
Name: "start_container",
|
||||
|
||||
@@ -28,11 +28,8 @@ 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 := findContainerFlexible(args.Host, args.ContainerID, hostService, labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("container not found: %w", err)
|
||||
}
|
||||
|
||||
@@ -155,11 +155,11 @@ 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 := findContainerFlexible(args.Host, args.ContainerID, hostService, labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("container not found: %w", err)
|
||||
}
|
||||
|
||||
@@ -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,32 @@ func logHostErrors(errs []error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// findContainerFlexible finds a container by ID, optionally scoped to a specific host.
|
||||
// When hostID is provided and valid, it uses direct lookup for efficiency.
|
||||
// Otherwise it searches across all hosts since container IDs are unique.
|
||||
func findContainerFlexible(hostID string, containerID string, hostService ToolHostService, labels container.ContainerLabels) (*container_support.ContainerService, error) {
|
||||
if containerID == "" {
|
||||
return nil, fmt.Errorf("container_id is required")
|
||||
}
|
||||
|
||||
// Try direct lookup if host is provided
|
||||
if hostID != "" {
|
||||
cs, err := hostService.FindContainer(hostID, containerID, labels)
|
||||
if err == nil {
|
||||
return cs, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to searching across all hosts
|
||||
containers, errs := hostService.ListAllContainers(labels)
|
||||
logHostErrors(errs)
|
||||
|
||||
for _, c := range containers {
|
||||
if c.ID == containerID {
|
||||
return hostService.FindContainer(c.Host, containerID, labels)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("container %s not found", containerID)
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ 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 := findContainerFlexible(args.Host, args.ContainerID, hostService, labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("container not found: %w", err)
|
||||
}
|
||||
|
||||
@@ -186,6 +186,52 @@ func TestExecuteTool_RestartContainer(t *testing.T) {
|
||||
mockClient.AssertCalled(t, "ContainerAction", mock.Anything, mock.Anything, container.Restart)
|
||||
}
|
||||
|
||||
func TestExecuteTool_RestartContainer_WithoutHostID(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"})
|
||||
|
||||
mockHost := &MockHostService{}
|
||||
// First call with empty host fails, then ListAllContainers finds the container on "local"
|
||||
mockHost.On("ListAllContainers", container.ContainerLabels(nil)).Return([]container.Container{
|
||||
{ID: "abc123", Name: "nginx", Image: "nginx:latest", State: "running", Host: "local"},
|
||||
}, nil)
|
||||
mockHost.On("FindContainer", "local", "abc123", container.ContainerLabels(nil)).Return(cs, nil)
|
||||
|
||||
argsJSON := `{"container_id": "abc123"}`
|
||||
resp := ExecuteTool(context.Background(), "restart_container", argsJSON, true, mockHost, nil)
|
||||
assert.True(t, resp.Success)
|
||||
|
||||
action := resp.GetAction()
|
||||
assert.NotNil(t, action)
|
||||
assert.True(t, action.Success)
|
||||
assert.Equal(t, "abc123", action.ContainerId)
|
||||
}
|
||||
|
||||
func TestExecuteTool_RestartContainer_WithWrongHost(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"})
|
||||
|
||||
mockHost := &MockHostService{}
|
||||
// LLM passes wrong host value, direct lookup fails, falls back to searching all hosts
|
||||
mockHost.On("FindContainer", "my-server", "abc123", container.ContainerLabels(nil)).Return(nil, fmt.Errorf("host not found"))
|
||||
mockHost.On("ListAllContainers", container.ContainerLabels(nil)).Return([]container.Container{
|
||||
{ID: "abc123", Name: "nginx", Image: "nginx:latest", State: "running", Host: "local"},
|
||||
}, nil)
|
||||
mockHost.On("FindContainer", "local", "abc123", container.ContainerLabels(nil)).Return(cs, nil)
|
||||
|
||||
argsJSON := `{"container_id": "abc123", "host_id": "my-server"}`
|
||||
resp := ExecuteTool(context.Background(), "restart_container", argsJSON, true, mockHost, nil)
|
||||
assert.True(t, resp.Success)
|
||||
|
||||
action := resp.GetAction()
|
||||
assert.NotNil(t, action)
|
||||
assert.True(t, action.Success)
|
||||
}
|
||||
|
||||
func TestExecuteTool_RestartContainer_ActionsDisabled(t *testing.T) {
|
||||
mockHost := &MockHostService{}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user