mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 10:14:44 +00:00
feat: add descriptive messages to cloud action responses (#4592)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -367,21 +367,26 @@ func (c *Client) ContainerAction(ctx context.Context, containerId string, action
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) UpdateContainer(ctx context.Context, containerID string, progressCh chan<- container.UpdateProgress) error {
|
||||
func (c *Client) UpdateContainer(ctx context.Context, containerID string, progressCh chan<- container.UpdateProgress) (bool, error) {
|
||||
defer close(progressCh)
|
||||
|
||||
stream, err := c.client.UpdateContainer(ctx, &pb.UpdateContainerRequest{ContainerId: containerID})
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
updated := false
|
||||
for {
|
||||
progress, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
return updated, nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
if progress.Status == "done" {
|
||||
updated = true
|
||||
}
|
||||
|
||||
progressCh <- container.UpdateProgress{
|
||||
|
||||
@@ -100,9 +100,9 @@ func (m *MockedClientService) Exec(ctx context.Context, c container.Container, c
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockedClientService) UpdateContainer(ctx context.Context, c container.Container, progressCh chan<- container.UpdateProgress) error {
|
||||
func (m *MockedClientService) UpdateContainer(ctx context.Context, c container.Container, progressCh chan<- container.UpdateProgress) (bool, error) {
|
||||
args := m.Called(ctx, c, progressCh)
|
||||
return args.Error(0)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
var wantedContainer = container.Container{}
|
||||
|
||||
@@ -39,7 +39,7 @@ type ClientService interface {
|
||||
ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error)
|
||||
Host(ctx context.Context) (container.Host, error)
|
||||
ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error
|
||||
UpdateContainer(ctx context.Context, container container.Container, progressCh chan<- container.UpdateProgress) error
|
||||
UpdateContainer(ctx context.Context, container container.Container, progressCh chan<- container.UpdateProgress) (bool, error)
|
||||
LogsBetweenDates(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error)
|
||||
RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error)
|
||||
SubscribeStats(context.Context, chan<- container.ContainerStat)
|
||||
@@ -324,7 +324,8 @@ func (s *server) UpdateContainer(req *pb.UpdateContainerRequest, out pb.AgentSer
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
errCh <- s.service.UpdateContainer(out.Context(), c, progressCh)
|
||||
_, err := s.service.UpdateContainer(out.Context(), c, progressCh)
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
for progress := range progressCh {
|
||||
|
||||
@@ -41,12 +41,15 @@ func executeContainerAction(ctx context.Context, name string, argsJSON string, h
|
||||
return nil, fmt.Errorf("action failed: %w", err)
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Successfully %s container %s.", pastTense(action), args.ContainerID)
|
||||
|
||||
return &pb.CallToolResponse{
|
||||
Success: true,
|
||||
Result: &pb.CallToolResponse_Action{Action: &pb.ActionResult{
|
||||
Success: true,
|
||||
ContainerId: args.ContainerID,
|
||||
Action: string(action),
|
||||
Message: message,
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
@@ -69,33 +72,24 @@ func executeUpdateContainer(ctx context.Context, argsJSON string, hostService To
|
||||
return nil, fmt.Errorf("container not found: %w", err)
|
||||
}
|
||||
|
||||
progressCh := make(chan container.UpdateProgress, 100)
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
progressCh := make(chan container.UpdateProgress)
|
||||
var updated bool
|
||||
var updateErr error
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
errCh <- cs.Update(ctx, progressCh)
|
||||
updated, updateErr = cs.Update(ctx, progressCh)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Drain progress channel and capture final status
|
||||
var lastStatus string
|
||||
var lastError string
|
||||
for progress := range progressCh {
|
||||
lastStatus = progress.Status
|
||||
if progress.Error != "" {
|
||||
lastError = progress.Error
|
||||
}
|
||||
for range progressCh {
|
||||
}
|
||||
<-done
|
||||
if updateErr != nil {
|
||||
return nil, fmt.Errorf("update failed: %w", updateErr)
|
||||
}
|
||||
|
||||
if err := <-errCh; err != nil {
|
||||
return nil, fmt.Errorf("update failed: %w", err)
|
||||
}
|
||||
|
||||
action := "update"
|
||||
if lastStatus == "up-to-date" {
|
||||
action = "update (already up-to-date)"
|
||||
}
|
||||
if lastError != "" {
|
||||
return nil, fmt.Errorf("update failed: %s", lastError)
|
||||
message := fmt.Sprintf("Successfully updated container %s by pulling the latest image and recreating it.", args.ContainerID)
|
||||
if !updated {
|
||||
message = fmt.Sprintf("Container %s is already running the latest image. No update was needed.", args.ContainerID)
|
||||
}
|
||||
|
||||
return &pb.CallToolResponse{
|
||||
@@ -103,11 +97,25 @@ func executeUpdateContainer(ctx context.Context, argsJSON string, hostService To
|
||||
Result: &pb.CallToolResponse_Action{Action: &pb.ActionResult{
|
||||
Success: true,
|
||||
ContainerId: args.ContainerID,
|
||||
Action: action,
|
||||
Action: "update",
|
||||
Message: message,
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func pastTense(action container.ContainerAction) string {
|
||||
switch action {
|
||||
case container.Start:
|
||||
return "started"
|
||||
case container.Stop:
|
||||
return "stopped"
|
||||
case container.Restart:
|
||||
return "restarted"
|
||||
default:
|
||||
return string(action) + "ed"
|
||||
}
|
||||
}
|
||||
|
||||
func resolveAction(name string) (container.ContainerAction, error) {
|
||||
switch name {
|
||||
case "start_container":
|
||||
|
||||
@@ -126,9 +126,9 @@ func (m *MockClientService) Exec(_ context.Context, _ container.Container, _ []s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockClientService) UpdateContainer(_ context.Context, _ container.Container, progressCh chan<- container.UpdateProgress) error {
|
||||
func (m *MockClientService) UpdateContainer(_ context.Context, _ container.Container, progressCh chan<- container.UpdateProgress) (bool, error) {
|
||||
close(progressCh)
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func TestExecuteTool_ListRunningContainers(t *testing.T) {
|
||||
|
||||
@@ -76,7 +76,7 @@ func (a *agentService) ContainerAction(ctx context.Context, container container.
|
||||
return a.client.ContainerAction(ctx, container.ID, action)
|
||||
}
|
||||
|
||||
func (a *agentService) UpdateContainer(ctx context.Context, c container.Container, progressCh chan<- container.UpdateProgress) error {
|
||||
func (a *agentService) UpdateContainer(ctx context.Context, c container.Container, progressCh chan<- container.UpdateProgress) (bool, error) {
|
||||
return a.client.UpdateContainer(ctx, c.ID, progressCh)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ type ClientService interface {
|
||||
ListContainers(ctx context.Context, filter container.ContainerLabels) ([]container.Container, error)
|
||||
Host(ctx context.Context) (container.Host, error)
|
||||
ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error
|
||||
UpdateContainer(ctx context.Context, container container.Container, progressCh chan<- container.UpdateProgress) error
|
||||
UpdateContainer(ctx context.Context, container container.Container, progressCh chan<- container.UpdateProgress) (bool, error)
|
||||
LogsBetweenDates(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error)
|
||||
RawLogs(context.Context, container.Container, time.Time, time.Time, container.StdType) (io.ReadCloser, error)
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ func (c *ContainerService) Action(ctx context.Context, action container.Containe
|
||||
return c.clientService.ContainerAction(ctx, c.Container, action)
|
||||
}
|
||||
|
||||
func (c *ContainerService) Update(ctx context.Context, progressCh chan<- container.UpdateProgress) error {
|
||||
func (c *ContainerService) Update(ctx context.Context, progressCh chan<- container.UpdateProgress) (bool, error) {
|
||||
return c.clientService.UpdateContainer(ctx, c.Container, progressCh)
|
||||
}
|
||||
|
||||
|
||||
@@ -114,14 +114,14 @@ type pullEvent struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (d *DockerClientService) UpdateContainer(ctx context.Context, c container.Container, progressCh chan<- container.UpdateProgress) error {
|
||||
func (d *DockerClientService) UpdateContainer(ctx context.Context, c container.Container, progressCh chan<- container.UpdateProgress) (bool, error) {
|
||||
defer close(progressCh)
|
||||
|
||||
// 1. Inspect container to get full config
|
||||
inspectResp, err := d.client.ContainerInspect(ctx, c.ID)
|
||||
if err != nil {
|
||||
progressCh <- container.UpdateProgress{Status: "error", Error: fmt.Sprintf("inspect failed: %v", err)}
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
imageName := inspectResp.Config.Image
|
||||
@@ -130,7 +130,7 @@ func (d *DockerClientService) UpdateContainer(ctx context.Context, c container.C
|
||||
reader, err := d.client.ImagePull(ctx, imageName)
|
||||
if err != nil {
|
||||
progressCh <- container.UpdateProgress{Status: "error", Error: fmt.Sprintf("pull failed: %v", err)}
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
@@ -142,7 +142,7 @@ func (d *DockerClientService) UpdateContainer(ctx context.Context, c container.C
|
||||
break
|
||||
} else if err != nil {
|
||||
progressCh <- container.UpdateProgress{Status: "error", Error: fmt.Sprintf("pull decode failed: %v", err)}
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
progressCh <- container.UpdateProgress{
|
||||
@@ -160,7 +160,7 @@ func (d *DockerClientService) UpdateContainer(ctx context.Context, c container.C
|
||||
// 3. If no new layers, report up-to-date
|
||||
if !updated {
|
||||
progressCh <- container.UpdateProgress{Status: "up-to-date"}
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 4. Check if this is a swarm service
|
||||
@@ -170,10 +170,10 @@ func (d *DockerClientService) UpdateContainer(ctx context.Context, c container.C
|
||||
serviceID := c.Labels["com.docker.swarm.service.id"]
|
||||
if err := d.client.ServiceUpdate(ctx, serviceID, imageName); err != nil {
|
||||
progressCh <- container.UpdateProgress{Status: "error", Error: fmt.Sprintf("service update failed: %v", err)}
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
progressCh <- container.UpdateProgress{Status: "done"}
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 5. Standalone container: stop -> remove -> create -> start
|
||||
@@ -185,31 +185,31 @@ func (d *DockerClientService) UpdateContainer(ctx context.Context, c container.C
|
||||
if c.State == "running" {
|
||||
if err := d.client.ContainerActions(ctx, container.Stop, c.ID); err != nil {
|
||||
progressCh <- container.UpdateProgress{Status: "error", Error: fmt.Sprintf("stop failed: %v", err)}
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// Remove
|
||||
if err := d.client.ContainerRemove(ctx, c.ID); err != nil {
|
||||
progressCh <- container.UpdateProgress{Status: "error", Error: fmt.Sprintf("remove failed: %v", err)}
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Create with same config
|
||||
newID, err := d.client.ContainerCreate(ctx, inspectResp, containerName)
|
||||
if err != nil {
|
||||
progressCh <- container.UpdateProgress{Status: "error", Error: fmt.Sprintf("create failed: %v", err)}
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Start
|
||||
if err := d.client.ContainerActions(ctx, container.Start, newID); err != nil {
|
||||
progressCh <- container.UpdateProgress{Status: "error", Error: fmt.Sprintf("start failed: %v", err)}
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
progressCh <- container.UpdateProgress{Status: "done"}
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (d *DockerClientService) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
|
||||
|
||||
@@ -93,9 +93,9 @@ func (k *K8sClientService) SubscribeContainersStarted(ctx context.Context, conta
|
||||
k.store.SubscribeNewContainers(ctx, containers)
|
||||
}
|
||||
|
||||
func (k *K8sClientService) UpdateContainer(ctx context.Context, c container.Container, progressCh chan<- container.UpdateProgress) error {
|
||||
func (k *K8sClientService) UpdateContainer(ctx context.Context, c container.Container, progressCh chan<- container.UpdateProgress) (bool, error) {
|
||||
defer close(progressCh)
|
||||
return fmt.Errorf("update container is not supported in Kubernetes mode")
|
||||
return false, fmt.Errorf("update container is not supported in Kubernetes mode")
|
||||
}
|
||||
|
||||
func (k *K8sClientService) Attach(ctx context.Context, c container.Container, events container.ExecEventReader, stdout io.Writer) error {
|
||||
|
||||
@@ -83,7 +83,8 @@ func (h *handler) containerUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
errCh <- containerService.Update(r.Context(), progressCh)
|
||||
_, err := containerService.Update(r.Context(), progressCh)
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
for progress := range progressCh {
|
||||
|
||||
+11
-2
@@ -1338,6 +1338,7 @@ type ActionResult struct {
|
||||
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"`
|
||||
Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -1393,6 +1394,13 @@ func (x *ActionResult) GetAction() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ActionResult) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_cloud_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_cloud_proto_rawDesc = "" +
|
||||
@@ -1511,11 +1519,12 @@ const file_cloud_proto_rawDesc = "" +
|
||||
"\fnetwork_mode\x18\x12 \x01(\tR\vnetworkMode\x1a9\n" +
|
||||
"\vLabelsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x0e\x10\x0fR\x03env\"c\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x0e\x10\x0fR\x03env\"}\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" +
|
||||
"\x06action\x18\x03 \x01(\tR\x06action\x12\x18\n" +
|
||||
"\amessage\x18\x04 \x01(\tR\amessage2M\n" +
|
||||
"\x10CloudToolService\x129\n" +
|
||||
"\n" +
|
||||
"ToolStream\x12\x13.cloud.ToolResponse\x1a\x12.cloud.ToolRequest(\x010\x01B&Z$github.com/amir20/dozzle/proto/cloudb\x06proto3"
|
||||
|
||||
@@ -150,4 +150,5 @@ message ActionResult {
|
||||
bool success = 1;
|
||||
string container_id = 2;
|
||||
string action = 3;
|
||||
string message = 4;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user