refactor: replace JSON strings with typed proto messages in cloud tools (#4583)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-04-04 07:54:58 -07:00
committed by GitHub
parent 7225abec25
commit cfcaebe36a
6 changed files with 1152 additions and 285 deletions
+15 -24
View File
@@ -40,10 +40,10 @@ type Client struct {
apiKeyFunc func() string
target string
plaintext bool
toolSem *semaphore.Weighted
cachedToolsJSON []string
cachedToolsOnce sync.Once
startCh chan struct{}
toolSem *semaphore.Weighted
cachedTools []*pb.ToolDefinition
toolsOnce sync.Once
startCh chan struct{}
}
// NewClient creates a new cloud gRPC client.
@@ -254,29 +254,20 @@ func (c *Client) handleRequest(ctx context.Context, req *pb.ToolRequest) *pb.Too
log.Debug().Str("request_id", req.RequestId).Msg("cloud requested tool list")
resp.Type = &pb.ToolResponse_ListTools{
ListTools: &pb.ListToolsResponse{
ToolsJson: c.toolsJSON(),
Tools: c.tools(),
},
}
case *pb.ToolRequest_CallTool:
log.Debug().Str("request_id", req.RequestId).Str("tool", t.CallTool.Name).Str("args", t.CallTool.ArgumentsJson).Msg("cloud tool call received")
result, err := ExecuteTool(ctx, t.CallTool.Name, t.CallTool.ArgumentsJson, c.enableActions, c.hostService, c.labels)
if err != nil {
log.Debug().Err(err).Str("request_id", req.RequestId).Str("tool", t.CallTool.Name).Msg("cloud tool call failed")
resp.Type = &pb.ToolResponse_CallTool{
CallTool: &pb.CallToolResponse{
Success: false,
Error: err.Error(),
},
}
callResp := ExecuteTool(ctx, t.CallTool.Name, t.CallTool.ArgumentsJson, c.enableActions, c.hostService, c.labels)
if !callResp.Success {
log.Debug().Str("error", callResp.Error).Str("request_id", req.RequestId).Str("tool", t.CallTool.Name).Msg("cloud tool call failed")
} else {
log.Debug().Str("request_id", req.RequestId).Str("tool", t.CallTool.Name).Msg("cloud tool call completed")
resp.Type = &pb.ToolResponse_CallTool{
CallTool: &pb.CallToolResponse{
Success: true,
ResultJson: result,
},
}
}
resp.Type = &pb.ToolResponse_CallTool{
CallTool: callResp,
}
default:
@@ -292,11 +283,11 @@ func (c *Client) handleRequest(ctx context.Context, req *pb.ToolRequest) *pb.Too
return resp
}
func (c *Client) toolsJSON() []string {
c.cachedToolsOnce.Do(func() {
c.cachedToolsJSON = marshalTools(c.enableActions)
func (c *Client) tools() []*pb.ToolDefinition {
c.toolsOnce.Do(func() {
c.cachedTools = AvailableTools(c.enableActions)
})
return c.cachedToolsJSON
return c.cachedTools
}
func isPermissionDenied(err error) bool {
+8 -5
View File
@@ -4,9 +4,9 @@ import (
"context"
"testing"
pb "github.com/amir20/dozzle/proto/cloud"
"github.com/amir20/dozzle/internal/container"
container_support "github.com/amir20/dozzle/internal/support/container"
pb "github.com/amir20/dozzle/proto/cloud"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
@@ -48,7 +48,7 @@ func TestHandleRequest_ListTools(t *testing.T) {
assert.Equal(t, "req-1", resp.RequestId)
listResp := resp.GetListTools()
assert.NotNil(t, listResp)
assert.Len(t, listResp.ToolsJson, 4) // find_containers + 3 actions
assert.Len(t, listResp.Tools, 7) // list_hosts + list_running_containers + list_all_containers + get_running_container_stats + 3 actions
}
func TestHandleRequest_ListTools_ActionsDisabled(t *testing.T) {
@@ -66,7 +66,7 @@ func TestHandleRequest_ListTools_ActionsDisabled(t *testing.T) {
resp := client.handleRequest(context.Background(), req)
listResp := resp.GetListTools()
assert.Len(t, listResp.ToolsJson, 1) // only list_containers
assert.Len(t, listResp.Tools, 4) // list_hosts + list_running_containers + list_all_containers + get_running_container_stats
}
func TestHandleRequest_CallTool_ListContainers(t *testing.T) {
@@ -83,7 +83,7 @@ func TestHandleRequest_CallTool_ListContainers(t *testing.T) {
RequestId: "req-3",
Type: &pb.ToolRequest_CallTool{
CallTool: &pb.CallToolRequest{
Name: "find_containers",
Name: "list_running_containers",
ArgumentsJson: "",
},
},
@@ -93,7 +93,10 @@ func TestHandleRequest_CallTool_ListContainers(t *testing.T) {
callResp := resp.GetCallTool()
assert.True(t, callResp.Success)
assert.Contains(t, callResp.ResultJson, "nginx")
result := callResp.GetListContainers()
assert.NotNil(t, result)
assert.Len(t, result.Containers, 1)
assert.Equal(t, "nginx", result.Containers[0].Name)
}
func TestHandleRequest_CallTool_UnknownTool(t *testing.T) {
+206 -177
View File
@@ -8,6 +8,7 @@ import (
"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"
)
@@ -15,56 +16,7 @@ import (
type ToolHostService interface {
ListAllContainers(labels container.ContainerLabels) ([]container.Container, []error)
FindContainer(host string, id string, labels container.ContainerLabels) (*container_support.ContainerService, error)
}
// FunctionDefinition describes a tool that can be called by the cloud service.
type FunctionDefinition struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters ParameterDefinition `json:"parameters"`
}
// ParameterDefinition describes the JSON Schema parameters for a tool.
type ParameterDefinition struct {
Type string `json:"type"`
Properties map[string]PropertyDefinition `json:"properties"`
Required []string `json:"required,omitempty"`
}
// PropertyDefinition describes a single property in a tool's parameters.
type PropertyDefinition struct {
Type string `json:"type"`
Description string `json:"description,omitempty"`
}
type containerResult struct {
ID string `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
Command string `json:"command"`
Created string `json:"created"`
StartedAt string `json:"startedAt"`
FinishedAt string `json:"finishedAt,omitempty"`
State string `json:"state"`
Health string `json:"health,omitempty"`
Host string `json:"host,omitempty"`
Group string `json:"group,omitempty"`
CPUPercent *float64 `json:"cpuPercent,omitempty"`
MaxCPU5Min *float64 `json:"maxCpu5Min,omitempty"`
MemoryPercent *float64 `json:"memoryPercent,omitempty"`
MaxMemory5Min *float64 `json:"maxMemory5Min,omitempty"`
}
var actionMap = map[string]container.ContainerAction{
"start_container": container.Start,
"stop_container": container.Stop,
"restart_container": container.Restart,
}
type actionResult struct {
Success bool `json:"success"`
ContainerID string `json:"containerId"`
Action string `json:"action"`
Hosts() []container.Host
}
type containerActionArgs struct {
@@ -73,49 +25,49 @@ type containerActionArgs struct {
}
// AvailableTools returns the list of tool definitions based on configuration.
// list_containers is always available. Action tools require enableActions.
func AvailableTools(enableActions bool) []FunctionDefinition {
tools := []FunctionDefinition{
func AvailableTools(enableActions bool) []*pb.ToolDefinition {
noParams := `{"type":"object","properties":{}}`
tools := []*pb.ToolDefinition{
{
Name: "find_containers",
Description: "List all Docker containers with their current state, name, image, and host",
Parameters: ParameterDefinition{
Type: "object",
Properties: map[string]PropertyDefinition{},
},
Name: "list_hosts",
Description: "List all Docker hosts connected to Dozzle with their name, CPU cores, total memory, Docker version, and availability status.",
ParametersJson: noParams,
},
{
Name: "list_running_containers",
Description: "List currently running Docker containers with their name, image, state, health status, and host. Only returns containers that are actively running.",
ParametersJson: noParams,
},
{
Name: "list_all_containers",
Description: "List all Docker containers including stopped, exited, and previously run containers. Includes finished time for non-running containers.",
ParametersJson: noParams,
},
{
Name: "get_running_container_stats",
Description: "Get real-time CPU and memory usage statistics for all currently running Docker containers. Returns current percentages and peak values over the last 5 minutes.",
ParametersJson: noParams,
},
}
if enableActions {
actionParams := ParameterDefinition{
Type: "object",
Properties: map[string]PropertyDefinition{
"container_id": {
Type: "string",
Description: "The container ID",
},
"host": {
Type: "string",
Description: "The host name where the container is running",
},
},
Required: []string{"container_id", "host"},
}
actionParams := `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID"},"host":{"type":"string","description":"The host name where the container is running"}},"required":["container_id","host"]}`
tools = append(tools,
FunctionDefinition{
Name: "start_container",
Description: "Start a stopped Docker container",
Parameters: actionParams,
&pb.ToolDefinition{
Name: "start_container",
Description: "Start a stopped Docker container",
ParametersJson: actionParams,
},
FunctionDefinition{
Name: "stop_container",
Description: "Stop a running Docker container",
Parameters: actionParams,
&pb.ToolDefinition{
Name: "stop_container",
Description: "Stop a running Docker container",
ParametersJson: actionParams,
},
FunctionDefinition{
Name: "restart_container",
Description: "Restart a Docker container",
Parameters: actionParams,
&pb.ToolDefinition{
Name: "restart_container",
Description: "Restart a Docker container",
ParametersJson: actionParams,
},
)
}
@@ -123,87 +75,192 @@ func AvailableTools(enableActions bool) []FunctionDefinition {
return tools
}
// marshalTools serializes tool definitions to JSON strings for the gRPC response.
func marshalTools(enableActions bool) []string {
tools := AvailableTools(enableActions)
result := make([]string, 0, len(tools))
for _, tool := range tools {
data, err := json.Marshal(tool)
if err != nil {
log.Error().Err(err).Str("tool", tool.Name).Msg("failed to marshal tool definition")
continue
}
result = append(result, string(data))
}
return result
}
// ExecuteTool dispatches a tool call by name and returns JSON result.
// ExecuteTool dispatches a tool call by name and returns a proto CallToolResponse.
// enableActions must be true for action tools (start/stop/restart) to execute.
func ExecuteTool(ctx context.Context, name string, argsJSON string, enableActions bool, hostService ToolHostService, labels container.ContainerLabels) (string, error) {
switch name {
case "find_containers":
if ctx.Err() != nil {
return "", ctx.Err()
func ExecuteTool(ctx context.Context, name string, argsJSON string, enableActions bool, hostService ToolHostService, labels container.ContainerLabels) *pb.CallToolResponse {
resp, err := executeTool(ctx, name, argsJSON, enableActions, hostService, labels)
if err != nil {
return &pb.CallToolResponse{
Success: false,
Error: err.Error(),
}
return executeListContainers(hostService, labels)
case "start_container", "stop_container", "restart_container":
if !enableActions {
return "", fmt.Errorf("container actions are not enabled")
}
return executeContainerAction(ctx, argsJSON, actionMap[name], hostService, labels)
default:
return "", fmt.Errorf("unknown tool: %s", name)
}
return resp
}
func executeListContainers(hostService ToolHostService, labels container.ContainerLabels) (string, error) {
containers, errs := hostService.ListAllContainers(labels)
for _, err := range errs {
if err != nil {
log.Warn().Err(err).Msg("error listing containers from host")
}
func executeTool(ctx context.Context, name string, argsJSON string, enableActions bool, hostService ToolHostService, labels container.ContainerLabels) (*pb.CallToolResponse, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
results := make([]containerResult, len(containers))
for i, c := range containers {
r := containerResult{
ID: c.ID,
Name: c.Name,
Image: c.Image,
Command: c.Command,
Created: c.Created.UTC().Format(time.RFC3339),
StartedAt: c.StartedAt.UTC().Format(time.RFC3339),
FinishedAt: formatTimeOrEmpty(c.FinishedAt),
State: c.State,
Health: c.Health,
Host: c.Host,
Group: c.Group,
switch name {
case "list_hosts":
hosts := hostService.Hosts()
result := make([]*pb.HostInfo, len(hosts))
for i, h := range hosts {
result[i] = &pb.HostInfo{
Id: h.ID,
Name: h.Name,
NCpu: int32(h.NCPU),
MemTotal: h.MemTotal,
DockerVersion: h.DockerVersion,
AgentVersion: h.AgentVersion,
Type: h.Type,
Available: h.Available,
}
}
return &pb.CallToolResponse{
Success: true,
Result: &pb.CallToolResponse_ListHosts{ListHosts: &pb.ListHostsResult{Hosts: result}},
}, nil
case "list_running_containers":
containers, errs := hostService.ListAllContainers(labels)
logHostErrors(errs)
result := make([]*pb.ContainerInfo, 0, len(containers))
for _, c := range containers {
if c.State != "running" {
continue
}
result = append(result, containerToProto(c))
}
return &pb.CallToolResponse{
Success: true,
Result: &pb.CallToolResponse_ListContainers{ListContainers: &pb.ListContainersResult{Containers: result}},
}, nil
case "list_all_containers":
containers, errs := hostService.ListAllContainers(labels)
logHostErrors(errs)
result := make([]*pb.ContainerInfo, 0, len(containers))
for _, c := range containers {
result = append(result, containerToProto(c))
}
return &pb.CallToolResponse{
Success: true,
Result: &pb.CallToolResponse_ListContainers{ListContainers: &pb.ListContainersResult{Containers: result}},
}, nil
case "get_running_container_stats":
containers, errs := hostService.ListAllContainers(labels)
logHostErrors(errs)
result := make([]*pb.ContainerStatEntry, 0, len(containers))
for _, c := range containers {
if c.State != "running" {
continue
}
if c.Stats == nil || c.Stats.Len() == 0 {
continue
}
if c.Stats != nil && c.Stats.Len() > 0 {
stats := c.Stats.Data()
latest := stats[len(stats)-1]
r.CPUPercent = &latest.CPUPercent
r.MemoryPercent = &latest.MemoryPercent
var maxCPU, maxMem float64
for _, s := range stats {
maxCPU = max(maxCPU, s.CPUPercent)
maxMem = max(maxMem, s.MemoryPercent)
}
r.MaxCPU5Min = &maxCPU
r.MaxMemory5Min = &maxMem
result = append(result, &pb.ContainerStatEntry{
Id: c.ID,
Name: c.Name,
Host: c.Host,
CpuPercent: latest.CPUPercent,
MemoryPercent: latest.MemoryPercent,
MemoryUsage: latest.MemoryUsage,
MaxCpu_5Min: maxCPU,
MaxMemory_5Min: maxMem,
})
}
return &pb.CallToolResponse{
Success: true,
Result: &pb.CallToolResponse_ContainerStats{ContainerStats: &pb.ContainerStatsResult{Stats: result}},
}, nil
case "start_container", "stop_container", "restart_container":
if !enableActions {
return nil, fmt.Errorf("container actions are not enabled")
}
var args containerActionArgs
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
return nil, fmt.Errorf("failed to parse arguments: %w", err)
}
results[i] = r
action, err := resolveAction(name)
if err != nil {
return nil, err
}
actionResult, err := executeAction(ctx, args.Host, args.ContainerID, action, hostService, labels)
if err != nil {
return nil, err
}
return &pb.CallToolResponse{
Success: true,
Result: &pb.CallToolResponse_Action{Action: actionResult},
}, nil
default:
return nil, fmt.Errorf("unknown tool: %s", name)
}
}
func resolveAction(name string) (container.ContainerAction, error) {
switch name {
case "start_container":
return container.Start, nil
case "stop_container":
return container.Stop, nil
case "restart_container":
return container.Restart, nil
default:
return "", fmt.Errorf("unknown action: %s", name)
}
}
func executeAction(ctx context.Context, host, containerID string, action container.ContainerAction, hostService ToolHostService, labels container.ContainerLabels) (*pb.ActionResult, error) {
if containerID == "" {
return nil, fmt.Errorf("container_id is required")
}
data, err := json.Marshal(results)
if err != nil {
return "", fmt.Errorf("failed to marshal containers: %w", err)
if host == "" {
return nil, fmt.Errorf("host is required")
}
cs, err := hostService.FindContainer(host, containerID, labels)
if err != nil {
return nil, fmt.Errorf("container not found: %w", err)
}
if err := cs.Action(ctx, action); err != nil {
return nil, fmt.Errorf("action failed: %w", err)
}
return &pb.ActionResult{
Success: true,
ContainerId: containerID,
Action: string(action),
}, nil
}
func containerToProto(c container.Container) *pb.ContainerInfo {
return &pb.ContainerInfo{
Id: c.ID,
Name: c.Name,
Image: c.Image,
Command: c.Command,
Created: c.Created.UTC().Format(time.RFC3339),
StartedAt: c.StartedAt.UTC().Format(time.RFC3339),
FinishedAt: formatTimeOrEmpty(c.FinishedAt),
State: c.State,
Health: c.Health,
Host: c.Host,
Group: c.Group,
}
return string(data), nil
}
func formatTimeOrEmpty(t time.Time) string {
@@ -213,38 +270,10 @@ func formatTimeOrEmpty(t time.Time) string {
return t.UTC().Format(time.RFC3339)
}
func executeContainerAction(ctx context.Context, argsJSON string, action container.ContainerAction, hostService ToolHostService, labels container.ContainerLabels) (string, error) {
var args containerActionArgs
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
return "", fmt.Errorf("failed to parse arguments: %w", err)
func logHostErrors(errs []error) {
for _, err := range errs {
if err != nil {
log.Warn().Err(err).Msg("error listing containers from host")
}
}
if args.ContainerID == "" {
return "", fmt.Errorf("container_id is required")
}
if args.Host == "" {
return "", fmt.Errorf("host is required")
}
cs, err := hostService.FindContainer(args.Host, args.ContainerID, labels)
if err != nil {
return "", fmt.Errorf("container not found: %w", err)
}
if err := cs.Action(ctx, action); err != nil {
return "", fmt.Errorf("action failed: %w", err)
}
result := actionResult{
Success: true,
ContainerID: args.ContainerID,
Action: string(action),
}
data, err := json.Marshal(result)
if err != nil {
return "", fmt.Errorf("failed to marshal result: %w", err)
}
return string(data), nil
}
+80 -31
View File
@@ -2,7 +2,6 @@ package cloud
import (
"context"
"encoding/json"
"fmt"
"io"
"testing"
@@ -22,11 +21,14 @@ func TestAvailableTools_WithActionsEnabled(t *testing.T) {
names[i] = tool.Name
}
assert.Contains(t, names, "find_containers")
assert.Contains(t, names, "list_hosts")
assert.Contains(t, names, "list_running_containers")
assert.Contains(t, names, "list_all_containers")
assert.Contains(t, names, "get_running_container_stats")
assert.Contains(t, names, "start_container")
assert.Contains(t, names, "stop_container")
assert.Contains(t, names, "restart_container")
assert.Len(t, tools, 4)
assert.Len(t, tools, 7)
}
func TestAvailableTools_WithActionsDisabled(t *testing.T) {
@@ -37,8 +39,11 @@ func TestAvailableTools_WithActionsDisabled(t *testing.T) {
names[i] = tool.Name
}
assert.Contains(t, names, "find_containers")
assert.Len(t, tools, 1)
assert.Contains(t, names, "list_hosts")
assert.Contains(t, names, "list_running_containers")
assert.Contains(t, names, "list_all_containers")
assert.Contains(t, names, "get_running_container_stats")
assert.Len(t, tools, 4)
}
func TestAvailableTools_ParametersAreValid(t *testing.T) {
@@ -47,7 +52,7 @@ func TestAvailableTools_ParametersAreValid(t *testing.T) {
for _, tool := range tools {
assert.NotEmpty(t, tool.Name)
assert.NotEmpty(t, tool.Description)
assert.NotNil(t, tool.Parameters)
assert.NotEmpty(t, tool.ParametersJson)
}
}
@@ -74,6 +79,11 @@ func (m *MockHostService) FindContainer(host string, id string, labels container
return args.Get(0).(*container_support.ContainerService), args.Error(1)
}
func (m *MockHostService) Hosts() []container.Host {
args := m.Called()
return args.Get(0).([]container.Host)
}
type MockClientService struct {
mock.Mock
}
@@ -112,22 +122,39 @@ func (m *MockClientService) Exec(_ context.Context, _ container.Container, _ []s
return nil
}
func TestExecuteTool_ListContainers(t *testing.T) {
func TestExecuteTool_ListRunningContainers(t *testing.T) {
mockHost := &MockHostService{}
mockHost.On("ListAllContainers", container.ContainerLabels(nil)).Return([]container.Container{
{ID: "abc123", Name: "nginx", Image: "nginx:latest", State: "running", Host: "local"},
{ID: "def456", Name: "redis", Image: "redis:7", State: "running", Host: "local"},
{ID: "ghi789", Name: "stopped", Image: "alpine:latest", State: "exited", Host: "local"},
}, nil)
result, err := ExecuteTool(context.Background(), "find_containers", "", false, mockHost, nil)
assert.NoError(t, err)
resp := ExecuteTool(context.Background(), "list_running_containers", "", false, mockHost, nil)
assert.True(t, resp.Success)
var containers []map[string]any
err = json.Unmarshal([]byte(result), &containers)
assert.NoError(t, err)
assert.Len(t, containers, 2)
assert.Equal(t, "abc123", containers[0]["id"])
assert.Equal(t, "nginx", containers[0]["name"])
result := resp.GetListContainers()
assert.NotNil(t, result)
assert.Len(t, result.Containers, 2)
assert.Equal(t, "abc123", result.Containers[0].Id)
assert.Equal(t, "nginx", result.Containers[0].Name)
}
func TestExecuteTool_ListAllContainers(t *testing.T) {
mockHost := &MockHostService{}
mockHost.On("ListAllContainers", container.ContainerLabels(nil)).Return([]container.Container{
{ID: "abc123", Name: "nginx", Image: "nginx:latest", State: "running", Host: "local"},
{ID: "def456", Name: "redis", Image: "redis:7", State: "exited", Host: "local"},
}, nil)
resp := ExecuteTool(context.Background(), "list_all_containers", "", false, mockHost, nil)
assert.True(t, resp.Success)
result := resp.GetListContainers()
assert.NotNil(t, result)
assert.Len(t, result.Containers, 2)
assert.Equal(t, "abc123", result.Containers[0].Id)
assert.Equal(t, "def456", result.Containers[1].Id)
}
func TestExecuteTool_RestartContainer(t *testing.T) {
@@ -140,9 +167,13 @@ func TestExecuteTool_RestartContainer(t *testing.T) {
mockHost.On("FindContainer", "local", "abc123", container.ContainerLabels(nil)).Return(cs, nil)
argsJSON := `{"container_id": "abc123", "host": "local"}`
result, err := ExecuteTool(context.Background(), "restart_container", argsJSON, true, mockHost, nil)
assert.NoError(t, err)
assert.Contains(t, result, "success")
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)
mockClient.AssertCalled(t, "ContainerAction", mock.Anything, mock.Anything, container.Restart)
}
@@ -151,12 +182,12 @@ func TestExecuteTool_RestartContainer_ActionsDisabled(t *testing.T) {
mockHost := &MockHostService{}
argsJSON := `{"container_id": "abc123"}`
_, err := ExecuteTool(context.Background(), "restart_container", argsJSON, false, mockHost, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "container actions are not enabled")
resp := ExecuteTool(context.Background(), "restart_container", argsJSON, false, mockHost, nil)
assert.False(t, resp.Success)
assert.Contains(t, resp.Error, "container actions are not enabled")
}
func TestExecuteTool_ListContainers_PartialHostError(t *testing.T) {
func TestExecuteTool_ListRunningContainers_PartialHostError(t *testing.T) {
mockHost := &MockHostService{}
mockHost.On("ListAllContainers", container.ContainerLabels(nil)).Return(
[]container.Container{
@@ -165,19 +196,37 @@ func TestExecuteTool_ListContainers_PartialHostError(t *testing.T) {
[]error{fmt.Errorf("host2 unreachable")},
)
result, err := ExecuteTool(context.Background(), "find_containers", "", false, mockHost, nil)
assert.NoError(t, err)
resp := ExecuteTool(context.Background(), "list_running_containers", "", false, mockHost, nil)
assert.True(t, resp.Success)
var containers []map[string]any
err = json.Unmarshal([]byte(result), &containers)
assert.NoError(t, err)
assert.Len(t, containers, 1)
result := resp.GetListContainers()
assert.NotNil(t, result)
assert.Len(t, result.Containers, 1)
}
func TestExecuteTool_ListHosts(t *testing.T) {
mockHost := &MockHostService{}
mockHost.On("Hosts").Return([]container.Host{
{ID: "host1", Name: "server-1", NCPU: 4, MemTotal: 8589934592, DockerVersion: "24.0.7", Available: true},
{ID: "host2", Name: "server-2", NCPU: 8, MemTotal: 17179869184, DockerVersion: "25.0.1", Available: false},
})
resp := ExecuteTool(context.Background(), "list_hosts", "", false, mockHost, nil)
assert.True(t, resp.Success)
result := resp.GetListHosts()
assert.NotNil(t, result)
assert.Len(t, result.Hosts, 2)
assert.Equal(t, "host1", result.Hosts[0].Id)
assert.Equal(t, "server-1", result.Hosts[0].Name)
assert.Equal(t, true, result.Hosts[0].Available)
assert.Equal(t, false, result.Hosts[1].Available)
}
func TestExecuteTool_UnknownTool(t *testing.T) {
mockHost := &MockHostService{}
_, err := ExecuteTool(context.Background(), "unknown_tool", "", false, mockHost, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unknown tool")
resp := ExecuteTool(context.Background(), "unknown_tool", "", false, mockHost, nil)
assert.False(t, resp.Success)
assert.Contains(t, resp.Error, "unknown tool")
}
+771 -45
View File
@@ -239,7 +239,7 @@ func (*ListToolsRequest) Descriptor() ([]byte, []int) {
type ListToolsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ToolsJson []string `protobuf:"bytes,1,rep,name=tools_json,json=toolsJson,proto3" json:"tools_json,omitempty"`
Tools []*ToolDefinition `protobuf:"bytes,1,rep,name=tools,proto3" json:"tools,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -274,13 +274,73 @@ func (*ListToolsResponse) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{3}
}
func (x *ListToolsResponse) GetToolsJson() []string {
func (x *ListToolsResponse) GetTools() []*ToolDefinition {
if x != nil {
return x.ToolsJson
return x.Tools
}
return nil
}
type ToolDefinition struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
ParametersJson string `protobuf:"bytes,3,opt,name=parameters_json,json=parametersJson,proto3" json:"parameters_json,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ToolDefinition) Reset() {
*x = ToolDefinition{}
mi := &file_cloud_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ToolDefinition) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ToolDefinition) ProtoMessage() {}
func (x *ToolDefinition) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ToolDefinition.ProtoReflect.Descriptor instead.
func (*ToolDefinition) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{4}
}
func (x *ToolDefinition) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ToolDefinition) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *ToolDefinition) GetParametersJson() string {
if x != nil {
return x.ParametersJson
}
return ""
}
type CallToolRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
@@ -291,7 +351,7 @@ type CallToolRequest struct {
func (x *CallToolRequest) Reset() {
*x = CallToolRequest{}
mi := &file_cloud_proto_msgTypes[4]
mi := &file_cloud_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -303,7 +363,7 @@ func (x *CallToolRequest) String() string {
func (*CallToolRequest) ProtoMessage() {}
func (x *CallToolRequest) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[4]
mi := &file_cloud_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -316,7 +376,7 @@ func (x *CallToolRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CallToolRequest.ProtoReflect.Descriptor instead.
func (*CallToolRequest) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{4}
return file_cloud_proto_rawDescGZIP(), []int{5}
}
func (x *CallToolRequest) GetName() string {
@@ -334,17 +394,23 @@ func (x *CallToolRequest) GetArgumentsJson() string {
}
type CallToolResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
ResultJson string `protobuf:"bytes,2,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"`
Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
// Types that are valid to be assigned to Result:
//
// *CallToolResponse_ListHosts
// *CallToolResponse_ListContainers
// *CallToolResponse_ContainerStats
// *CallToolResponse_Action
Result isCallToolResponse_Result `protobuf_oneof:"result"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CallToolResponse) Reset() {
*x = CallToolResponse{}
mi := &file_cloud_proto_msgTypes[5]
mi := &file_cloud_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -356,7 +422,7 @@ func (x *CallToolResponse) String() string {
func (*CallToolResponse) ProtoMessage() {}
func (x *CallToolResponse) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[5]
mi := &file_cloud_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -369,7 +435,7 @@ func (x *CallToolResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CallToolResponse.ProtoReflect.Descriptor instead.
func (*CallToolResponse) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{5}
return file_cloud_proto_rawDescGZIP(), []int{6}
}
func (x *CallToolResponse) GetSuccess() bool {
@@ -379,16 +445,600 @@ func (x *CallToolResponse) GetSuccess() bool {
return false
}
func (x *CallToolResponse) GetResultJson() string {
func (x *CallToolResponse) GetError() string {
if x != nil {
return x.ResultJson
return x.Error
}
return ""
}
func (x *CallToolResponse) GetError() string {
func (x *CallToolResponse) GetResult() isCallToolResponse_Result {
if x != nil {
return x.Error
return x.Result
}
return nil
}
func (x *CallToolResponse) GetListHosts() *ListHostsResult {
if x != nil {
if x, ok := x.Result.(*CallToolResponse_ListHosts); ok {
return x.ListHosts
}
}
return nil
}
func (x *CallToolResponse) GetListContainers() *ListContainersResult {
if x != nil {
if x, ok := x.Result.(*CallToolResponse_ListContainers); ok {
return x.ListContainers
}
}
return nil
}
func (x *CallToolResponse) GetContainerStats() *ContainerStatsResult {
if x != nil {
if x, ok := x.Result.(*CallToolResponse_ContainerStats); ok {
return x.ContainerStats
}
}
return nil
}
func (x *CallToolResponse) GetAction() *ActionResult {
if x != nil {
if x, ok := x.Result.(*CallToolResponse_Action); ok {
return x.Action
}
}
return nil
}
type isCallToolResponse_Result interface {
isCallToolResponse_Result()
}
type CallToolResponse_ListHosts struct {
ListHosts *ListHostsResult `protobuf:"bytes,3,opt,name=list_hosts,json=listHosts,proto3,oneof"`
}
type CallToolResponse_ListContainers struct {
ListContainers *ListContainersResult `protobuf:"bytes,4,opt,name=list_containers,json=listContainers,proto3,oneof"`
}
type CallToolResponse_ContainerStats struct {
ContainerStats *ContainerStatsResult `protobuf:"bytes,5,opt,name=container_stats,json=containerStats,proto3,oneof"`
}
type CallToolResponse_Action struct {
Action *ActionResult `protobuf:"bytes,6,opt,name=action,proto3,oneof"`
}
func (*CallToolResponse_ListHosts) isCallToolResponse_Result() {}
func (*CallToolResponse_ListContainers) isCallToolResponse_Result() {}
func (*CallToolResponse_ContainerStats) isCallToolResponse_Result() {}
func (*CallToolResponse_Action) isCallToolResponse_Result() {}
// Host information
type HostInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
NCpu int32 `protobuf:"varint,3,opt,name=n_cpu,json=nCpu,proto3" json:"n_cpu,omitempty"`
MemTotal int64 `protobuf:"varint,4,opt,name=mem_total,json=memTotal,proto3" json:"mem_total,omitempty"`
DockerVersion string `protobuf:"bytes,5,opt,name=docker_version,json=dockerVersion,proto3" json:"docker_version,omitempty"`
AgentVersion string `protobuf:"bytes,6,opt,name=agent_version,json=agentVersion,proto3" json:"agent_version,omitempty"`
Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"`
Available bool `protobuf:"varint,8,opt,name=available,proto3" json:"available,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HostInfo) Reset() {
*x = HostInfo{}
mi := &file_cloud_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HostInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HostInfo) ProtoMessage() {}
func (x *HostInfo) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HostInfo.ProtoReflect.Descriptor instead.
func (*HostInfo) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{7}
}
func (x *HostInfo) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *HostInfo) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *HostInfo) GetNCpu() int32 {
if x != nil {
return x.NCpu
}
return 0
}
func (x *HostInfo) GetMemTotal() int64 {
if x != nil {
return x.MemTotal
}
return 0
}
func (x *HostInfo) GetDockerVersion() string {
if x != nil {
return x.DockerVersion
}
return ""
}
func (x *HostInfo) GetAgentVersion() string {
if x != nil {
return x.AgentVersion
}
return ""
}
func (x *HostInfo) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *HostInfo) GetAvailable() bool {
if x != nil {
return x.Available
}
return false
}
type ListHostsResult struct {
state protoimpl.MessageState `protogen:"open.v1"`
Hosts []*HostInfo `protobuf:"bytes,1,rep,name=hosts,proto3" json:"hosts,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListHostsResult) Reset() {
*x = ListHostsResult{}
mi := &file_cloud_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListHostsResult) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListHostsResult) ProtoMessage() {}
func (x *ListHostsResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListHostsResult.ProtoReflect.Descriptor instead.
func (*ListHostsResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{8}
}
func (x *ListHostsResult) GetHosts() []*HostInfo {
if x != nil {
return x.Hosts
}
return nil
}
// Container information
type ContainerInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Image string `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
Command string `protobuf:"bytes,4,opt,name=command,proto3" json:"command,omitempty"`
Created string `protobuf:"bytes,5,opt,name=created,proto3" json:"created,omitempty"`
StartedAt string `protobuf:"bytes,6,opt,name=started_at,json=startedAt,proto3" json:"started_at,omitempty"`
FinishedAt string `protobuf:"bytes,7,opt,name=finished_at,json=finishedAt,proto3" json:"finished_at,omitempty"`
State string `protobuf:"bytes,8,opt,name=state,proto3" json:"state,omitempty"`
Health string `protobuf:"bytes,9,opt,name=health,proto3" json:"health,omitempty"`
Host string `protobuf:"bytes,10,opt,name=host,proto3" json:"host,omitempty"`
Group string `protobuf:"bytes,11,opt,name=group,proto3" json:"group,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ContainerInfo) Reset() {
*x = ContainerInfo{}
mi := &file_cloud_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ContainerInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ContainerInfo) ProtoMessage() {}
func (x *ContainerInfo) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ContainerInfo.ProtoReflect.Descriptor instead.
func (*ContainerInfo) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{9}
}
func (x *ContainerInfo) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *ContainerInfo) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ContainerInfo) GetImage() string {
if x != nil {
return x.Image
}
return ""
}
func (x *ContainerInfo) GetCommand() string {
if x != nil {
return x.Command
}
return ""
}
func (x *ContainerInfo) GetCreated() string {
if x != nil {
return x.Created
}
return ""
}
func (x *ContainerInfo) GetStartedAt() string {
if x != nil {
return x.StartedAt
}
return ""
}
func (x *ContainerInfo) GetFinishedAt() string {
if x != nil {
return x.FinishedAt
}
return ""
}
func (x *ContainerInfo) GetState() string {
if x != nil {
return x.State
}
return ""
}
func (x *ContainerInfo) GetHealth() string {
if x != nil {
return x.Health
}
return ""
}
func (x *ContainerInfo) GetHost() string {
if x != nil {
return x.Host
}
return ""
}
func (x *ContainerInfo) GetGroup() string {
if x != nil {
return x.Group
}
return ""
}
type ListContainersResult struct {
state protoimpl.MessageState `protogen:"open.v1"`
Containers []*ContainerInfo `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListContainersResult) Reset() {
*x = ListContainersResult{}
mi := &file_cloud_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListContainersResult) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListContainersResult) ProtoMessage() {}
func (x *ListContainersResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListContainersResult.ProtoReflect.Descriptor instead.
func (*ListContainersResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{10}
}
func (x *ListContainersResult) GetContainers() []*ContainerInfo {
if x != nil {
return x.Containers
}
return nil
}
// Container stats
type ContainerStatEntry struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"`
CpuPercent float64 `protobuf:"fixed64,4,opt,name=cpu_percent,json=cpuPercent,proto3" json:"cpu_percent,omitempty"`
MemoryPercent float64 `protobuf:"fixed64,5,opt,name=memory_percent,json=memoryPercent,proto3" json:"memory_percent,omitempty"`
MemoryUsage float64 `protobuf:"fixed64,6,opt,name=memory_usage,json=memoryUsage,proto3" json:"memory_usage,omitempty"`
MaxCpu_5Min float64 `protobuf:"fixed64,7,opt,name=max_cpu_5min,json=maxCpu5min,proto3" json:"max_cpu_5min,omitempty"`
MaxMemory_5Min float64 `protobuf:"fixed64,8,opt,name=max_memory_5min,json=maxMemory5min,proto3" json:"max_memory_5min,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ContainerStatEntry) Reset() {
*x = ContainerStatEntry{}
mi := &file_cloud_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ContainerStatEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ContainerStatEntry) ProtoMessage() {}
func (x *ContainerStatEntry) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ContainerStatEntry.ProtoReflect.Descriptor instead.
func (*ContainerStatEntry) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{11}
}
func (x *ContainerStatEntry) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *ContainerStatEntry) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ContainerStatEntry) GetHost() string {
if x != nil {
return x.Host
}
return ""
}
func (x *ContainerStatEntry) GetCpuPercent() float64 {
if x != nil {
return x.CpuPercent
}
return 0
}
func (x *ContainerStatEntry) GetMemoryPercent() float64 {
if x != nil {
return x.MemoryPercent
}
return 0
}
func (x *ContainerStatEntry) GetMemoryUsage() float64 {
if x != nil {
return x.MemoryUsage
}
return 0
}
func (x *ContainerStatEntry) GetMaxCpu_5Min() float64 {
if x != nil {
return x.MaxCpu_5Min
}
return 0
}
func (x *ContainerStatEntry) GetMaxMemory_5Min() float64 {
if x != nil {
return x.MaxMemory_5Min
}
return 0
}
type ContainerStatsResult struct {
state protoimpl.MessageState `protogen:"open.v1"`
Stats []*ContainerStatEntry `protobuf:"bytes,1,rep,name=stats,proto3" json:"stats,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ContainerStatsResult) Reset() {
*x = ContainerStatsResult{}
mi := &file_cloud_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ContainerStatsResult) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ContainerStatsResult) ProtoMessage() {}
func (x *ContainerStatsResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ContainerStatsResult.ProtoReflect.Descriptor instead.
func (*ContainerStatsResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{12}
}
func (x *ContainerStatsResult) GetStats() []*ContainerStatEntry {
if x != nil {
return x.Stats
}
return nil
}
// Container action result
type ActionResult struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
ContainerId string `protobuf:"bytes,2,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"`
Action string `protobuf:"bytes,3,opt,name=action,proto3" json:"action,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ActionResult) Reset() {
*x = ActionResult{}
mi := &file_cloud_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ActionResult) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ActionResult) ProtoMessage() {}
func (x *ActionResult) ProtoReflect() protoreflect.Message {
mi := &file_cloud_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ActionResult.ProtoReflect.Descriptor instead.
func (*ActionResult) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{13}
}
func (x *ActionResult) GetSuccess() bool {
if x != nil {
return x.Success
}
return false
}
func (x *ActionResult) GetContainerId() string {
if x != nil {
return x.ContainerId
}
return ""
}
func (x *ActionResult) GetAction() string {
if x != nil {
return x.Action
}
return ""
}
@@ -412,18 +1062,72 @@ const file_cloud_proto_rawDesc = "" +
"list_tools\x18\x02 \x01(\v2\x18.cloud.ListToolsResponseH\x00R\tlistTools\x126\n" +
"\tcall_tool\x18\x03 \x01(\v2\x17.cloud.CallToolResponseH\x00R\bcallToolB\x06\n" +
"\x04type\"\x12\n" +
"\x10ListToolsRequest\"2\n" +
"\x11ListToolsResponse\x12\x1d\n" +
"\n" +
"tools_json\x18\x01 \x03(\tR\ttoolsJson\"L\n" +
"\x10ListToolsRequest\"@\n" +
"\x11ListToolsResponse\x12+\n" +
"\x05tools\x18\x01 \x03(\v2\x15.cloud.ToolDefinitionR\x05tools\"o\n" +
"\x0eToolDefinition\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12 \n" +
"\vdescription\x18\x02 \x01(\tR\vdescription\x12'\n" +
"\x0fparameters_json\x18\x03 \x01(\tR\x0eparametersJson\"L\n" +
"\x0fCallToolRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12%\n" +
"\x0earguments_json\x18\x02 \x01(\tR\rargumentsJson\"c\n" +
"\x0earguments_json\x18\x02 \x01(\tR\rargumentsJson\"\xc4\x02\n" +
"\x10CallToolResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1f\n" +
"\vresult_json\x18\x02 \x01(\tR\n" +
"resultJson\x12\x14\n" +
"\x05error\x18\x03 \x01(\tR\x05error2M\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x14\n" +
"\x05error\x18\x02 \x01(\tR\x05error\x127\n" +
"\n" +
"list_hosts\x18\x03 \x01(\v2\x16.cloud.ListHostsResultH\x00R\tlistHosts\x12F\n" +
"\x0flist_containers\x18\x04 \x01(\v2\x1b.cloud.ListContainersResultH\x00R\x0elistContainers\x12F\n" +
"\x0fcontainer_stats\x18\x05 \x01(\v2\x1b.cloud.ContainerStatsResultH\x00R\x0econtainerStats\x12-\n" +
"\x06action\x18\x06 \x01(\v2\x13.cloud.ActionResultH\x00R\x06actionB\b\n" +
"\x06result\"\xde\x01\n" +
"\bHostInfo\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x13\n" +
"\x05n_cpu\x18\x03 \x01(\x05R\x04nCpu\x12\x1b\n" +
"\tmem_total\x18\x04 \x01(\x03R\bmemTotal\x12%\n" +
"\x0edocker_version\x18\x05 \x01(\tR\rdockerVersion\x12#\n" +
"\ragent_version\x18\x06 \x01(\tR\fagentVersion\x12\x12\n" +
"\x04type\x18\a \x01(\tR\x04type\x12\x1c\n" +
"\tavailable\x18\b \x01(\bR\tavailable\"8\n" +
"\x0fListHostsResult\x12%\n" +
"\x05hosts\x18\x01 \x03(\v2\x0f.cloud.HostInfoR\x05hosts\"\x95\x02\n" +
"\rContainerInfo\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" +
"\x05image\x18\x03 \x01(\tR\x05image\x12\x18\n" +
"\acommand\x18\x04 \x01(\tR\acommand\x12\x18\n" +
"\acreated\x18\x05 \x01(\tR\acreated\x12\x1d\n" +
"\n" +
"started_at\x18\x06 \x01(\tR\tstartedAt\x12\x1f\n" +
"\vfinished_at\x18\a \x01(\tR\n" +
"finishedAt\x12\x14\n" +
"\x05state\x18\b \x01(\tR\x05state\x12\x16\n" +
"\x06health\x18\t \x01(\tR\x06health\x12\x12\n" +
"\x04host\x18\n" +
" \x01(\tR\x04host\x12\x14\n" +
"\x05group\x18\v \x01(\tR\x05group\"L\n" +
"\x14ListContainersResult\x124\n" +
"\n" +
"containers\x18\x01 \x03(\v2\x14.cloud.ContainerInfoR\n" +
"containers\"\x81\x02\n" +
"\x12ContainerStatEntry\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
"\x04host\x18\x03 \x01(\tR\x04host\x12\x1f\n" +
"\vcpu_percent\x18\x04 \x01(\x01R\n" +
"cpuPercent\x12%\n" +
"\x0ememory_percent\x18\x05 \x01(\x01R\rmemoryPercent\x12!\n" +
"\fmemory_usage\x18\x06 \x01(\x01R\vmemoryUsage\x12 \n" +
"\fmax_cpu_5min\x18\a \x01(\x01R\n" +
"maxCpu5min\x12&\n" +
"\x0fmax_memory_5min\x18\b \x01(\x01R\rmaxMemory5min\"G\n" +
"\x14ContainerStatsResult\x12/\n" +
"\x05stats\x18\x01 \x03(\v2\x19.cloud.ContainerStatEntryR\x05stats\"c\n" +
"\fActionResult\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12!\n" +
"\fcontainer_id\x18\x02 \x01(\tR\vcontainerId\x12\x16\n" +
"\x06action\x18\x03 \x01(\tR\x06action2M\n" +
"\x10CloudToolService\x129\n" +
"\n" +
"ToolStream\x12\x13.cloud.ToolResponse\x1a\x12.cloud.ToolRequest(\x010\x01B&Z$github.com/amir20/dozzle/proto/cloudb\x06proto3"
@@ -440,27 +1144,43 @@ func file_cloud_proto_rawDescGZIP() []byte {
return file_cloud_proto_rawDescData
}
var file_cloud_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_cloud_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
var file_cloud_proto_goTypes = []any{
(*ToolRequest)(nil), // 0: cloud.ToolRequest
(*ToolResponse)(nil), // 1: cloud.ToolResponse
(*ListToolsRequest)(nil), // 2: cloud.ListToolsRequest
(*ListToolsResponse)(nil), // 3: cloud.ListToolsResponse
(*CallToolRequest)(nil), // 4: cloud.CallToolRequest
(*CallToolResponse)(nil), // 5: cloud.CallToolResponse
(*ToolRequest)(nil), // 0: cloud.ToolRequest
(*ToolResponse)(nil), // 1: cloud.ToolResponse
(*ListToolsRequest)(nil), // 2: cloud.ListToolsRequest
(*ListToolsResponse)(nil), // 3: cloud.ListToolsResponse
(*ToolDefinition)(nil), // 4: cloud.ToolDefinition
(*CallToolRequest)(nil), // 5: cloud.CallToolRequest
(*CallToolResponse)(nil), // 6: cloud.CallToolResponse
(*HostInfo)(nil), // 7: cloud.HostInfo
(*ListHostsResult)(nil), // 8: cloud.ListHostsResult
(*ContainerInfo)(nil), // 9: cloud.ContainerInfo
(*ListContainersResult)(nil), // 10: cloud.ListContainersResult
(*ContainerStatEntry)(nil), // 11: cloud.ContainerStatEntry
(*ContainerStatsResult)(nil), // 12: cloud.ContainerStatsResult
(*ActionResult)(nil), // 13: cloud.ActionResult
}
var file_cloud_proto_depIdxs = []int32{
2, // 0: cloud.ToolRequest.list_tools:type_name -> cloud.ListToolsRequest
4, // 1: cloud.ToolRequest.call_tool:type_name -> cloud.CallToolRequest
3, // 2: cloud.ToolResponse.list_tools:type_name -> cloud.ListToolsResponse
5, // 3: cloud.ToolResponse.call_tool:type_name -> cloud.CallToolResponse
1, // 4: cloud.CloudToolService.ToolStream:input_type -> cloud.ToolResponse
0, // 5: cloud.CloudToolService.ToolStream:output_type -> cloud.ToolRequest
5, // [5:6] is the sub-list for method output_type
4, // [4:5] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
2, // 0: cloud.ToolRequest.list_tools:type_name -> cloud.ListToolsRequest
5, // 1: cloud.ToolRequest.call_tool:type_name -> cloud.CallToolRequest
3, // 2: cloud.ToolResponse.list_tools:type_name -> cloud.ListToolsResponse
6, // 3: cloud.ToolResponse.call_tool:type_name -> cloud.CallToolResponse
4, // 4: cloud.ListToolsResponse.tools:type_name -> cloud.ToolDefinition
8, // 5: cloud.CallToolResponse.list_hosts:type_name -> cloud.ListHostsResult
10, // 6: cloud.CallToolResponse.list_containers:type_name -> cloud.ListContainersResult
12, // 7: cloud.CallToolResponse.container_stats:type_name -> cloud.ContainerStatsResult
13, // 8: cloud.CallToolResponse.action:type_name -> cloud.ActionResult
7, // 9: cloud.ListHostsResult.hosts:type_name -> cloud.HostInfo
9, // 10: cloud.ListContainersResult.containers:type_name -> cloud.ContainerInfo
11, // 11: cloud.ContainerStatsResult.stats:type_name -> cloud.ContainerStatEntry
1, // 12: cloud.CloudToolService.ToolStream:input_type -> cloud.ToolResponse
0, // 13: cloud.CloudToolService.ToolStream:output_type -> cloud.ToolRequest
13, // [13:14] is the sub-list for method output_type
12, // [12:13] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
}
func init() { file_cloud_proto_init() }
@@ -476,13 +1196,19 @@ func file_cloud_proto_init() {
(*ToolResponse_ListTools)(nil),
(*ToolResponse_CallTool)(nil),
}
file_cloud_proto_msgTypes[6].OneofWrappers = []any{
(*CallToolResponse_ListHosts)(nil),
(*CallToolResponse_ListContainers)(nil),
(*CallToolResponse_ContainerStats)(nil),
(*CallToolResponse_Action)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_cloud_proto_rawDesc), len(file_cloud_proto_rawDesc)),
NumEnums: 0,
NumMessages: 6,
NumMessages: 14,
NumExtensions: 0,
NumServices: 1,
},
+72 -3
View File
@@ -28,7 +28,13 @@ message ToolResponse {
message ListToolsRequest {}
message ListToolsResponse {
repeated string tools_json = 1;
repeated ToolDefinition tools = 1;
}
message ToolDefinition {
string name = 1;
string description = 2;
string parameters_json = 3;
}
message CallToolRequest {
@@ -38,6 +44,69 @@ message CallToolRequest {
message CallToolResponse {
bool success = 1;
string result_json = 2;
string error = 3;
string error = 2;
oneof result {
ListHostsResult list_hosts = 3;
ListContainersResult list_containers = 4;
ContainerStatsResult container_stats = 5;
ActionResult action = 6;
}
}
// Host information
message HostInfo {
string id = 1;
string name = 2;
int32 n_cpu = 3;
int64 mem_total = 4;
string docker_version = 5;
string agent_version = 6;
string type = 7;
bool available = 8;
}
message ListHostsResult {
repeated HostInfo hosts = 1;
}
// Container information
message ContainerInfo {
string id = 1;
string name = 2;
string image = 3;
string command = 4;
string created = 5;
string started_at = 6;
string finished_at = 7;
string state = 8;
string health = 9;
string host = 10;
string group = 11;
}
message ListContainersResult {
repeated ContainerInfo containers = 1;
}
// Container stats
message ContainerStatEntry {
string id = 1;
string name = 2;
string host = 3;
double cpu_percent = 4;
double memory_percent = 5;
double memory_usage = 6;
double max_cpu_5min = 7;
double max_memory_5min = 8;
}
message ContainerStatsResult {
repeated ContainerStatEntry stats = 1;
}
// Container action result
message ActionResult {
bool success = 1;
string container_id = 2;
string action = 3;
}