feat: add container update action with image pull and recreate 🚀 (#4588)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-04-05 12:59:44 -07:00
committed by GitHub
parent 475a16321d
commit 284822c631
43 changed files with 1149 additions and 119 deletions
@@ -28,3 +28,10 @@
- Subscription fields use atomic types: TriggerCount (atomic.Int64), LastTriggeredAt (atomic.Pointer)
- MetricCooldowns uses xsync.Map for per-container cooldown tracking
- sendSem (semaphore.Weighted=5) limits concurrent notification sends
### Container Update (feat/container-update)
- **progressCh close contract**: Docker and Agent implementations close progressCh via defer; K8s does NOT, causing handler hang
- **NetworkSettings nil risk**: `docker.InspectResponse.NetworkSettings` is a pointer; `ContainerCreate` doesn't nil-check before accessing `.Networks`
- **Destructive recreate**: stop->remove->create->start has no rollback if create fails after remove
- **SSE parsing in frontend**: Uses manual ReadableStream reader, not EventSource; no AbortController cleanup on unmount
+1
View File
@@ -27,6 +27,7 @@ declare module 'vue' {
'Carbon:star': typeof import('~icons/carbon/star')['default']
'Carbon:starFilled': typeof import('~icons/carbon/star-filled')['default']
'Carbon:stopFilledAlt': typeof import('~icons/carbon/stop-filled-alt')['default']
'Carbon:upgrade': typeof import('~icons/carbon/upgrade')['default']
'Carbon:warning': typeof import('~icons/carbon/warning')['default']
Carousel: typeof import('./components/common/Carousel.vue')['default']
CarouselItem: typeof import('./components/common/CarouselItem.vue')['default']
@@ -154,6 +154,17 @@
{{ $t("toolbar.restart") }}
</button>
</li>
<li>
<button @click="update()" :disabled="actionStates.update">
<carbon:upgrade
:class="{
'animate-spin': actionStates.update,
'text-secondary': actionStates.update,
}"
/>
{{ container.isSwarm ? $t("toolbar.update-service") : $t("toolbar.update") }}
</button>
</li>
</template>
<template v-if="enableShell && !historical">
@@ -190,7 +201,7 @@ const showDrawer = useDrawer();
const { container, historical = false } = defineProps<{ container: Container; historical?: boolean }>();
const clear = defineEmit();
const { actionStates, start, stop, restart } = useContainerActions(toRef(() => container));
const { actionStates, start, stop, restart, update } = useContainerActions(toRef(() => container));
const router = useRouter();
const { copy, copied, isSupported } = useClipboard();
+101 -6
View File
@@ -2,25 +2,27 @@ import { Container } from "@/models/Container";
type ContainerActions = "start" | "stop" | "restart";
export const useContainerActions = (container: Ref<Container>) => {
const { showToast } = useToast();
const { showToast, removeToast } = useToast();
const { t } = useI18n();
const actionStates = reactive({
stop: false,
restart: false,
start: false,
update: false,
});
async function actionHandler(action: ContainerActions) {
const actionUrl = `/api/hosts/${container.value.host}/containers/${container.value.id}/actions/${action}`;
const errors = {
404: "container not found",
500: "unable to complete action",
400: "invalid action",
404: t("error.container-not-found"),
500: t("error.unable-to-complete-action"),
400: t("error.invalid-action"),
} as Record<number, string>;
const defaultError = "something went wrong";
const toastTitle = "Action Failed";
const defaultError = t("error.something-went-wrong");
const toastTitle = t("error.action-failed");
actionStates[action] = true;
@@ -37,10 +39,103 @@ export const useContainerActions = (container: Ref<Container>) => {
actionStates[action] = false;
}
async function update() {
const updateUrl = `/api/hosts/${container.value.host}/containers/${container.value.id}/actions/update`;
const toastId = "container-update";
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
actionStates.update = true;
showToast(
{
id: toastId,
title: t("toolbar.update"),
message: t("toolbar.update-pulling"),
type: "info",
},
{ once: true },
);
try {
const response = await fetch(withBase(updateUrl), { method: "POST" });
if (!response.ok) {
removeToast(toastId);
showToast({ type: "error", message: t("error.unable-to-update"), title: t("error.update-failed") });
return;
}
reader = response.body?.getReader();
if (!reader) return;
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n\n");
buffer = lines.pop() ?? "";
for (const chunk of lines) {
const dataLine = chunk.split("\n").find((l) => l.startsWith("data: "));
if (!dataLine) continue;
const data = JSON.parse(dataLine.slice(6));
switch (data.status) {
case "pulling":
break;
case "recreating":
removeToast(toastId);
showToast(
{
id: toastId,
title: t("toolbar.update"),
message: t("toolbar.update-recreating"),
type: "info",
},
{ once: true },
);
break;
case "done":
case "up-to-date":
removeToast(toastId);
showToast(
{
title: t("toolbar.update"),
message: t(`toolbar.update-${data.status}`),
type: "info",
},
{ expire: 3000 },
);
break;
case "error":
removeToast(toastId);
showToast({
type: "error",
message: data.error || t("error.unknown-error"),
title: t("error.update-failed"),
});
break;
}
}
}
} catch (error) {
removeToast(toastId);
showToast({ type: "error", message: t("error.something-went-wrong"), title: t("error.update-failed") });
} finally {
reader?.cancel();
actionStates.update = false;
}
}
return {
actionStates,
start: () => actionHandler("start"),
stop: () => actionHandler("stop"),
restart: () => actionHandler("restart"),
update,
};
};
+27
View File
@@ -367,6 +367,33 @@ 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 {
defer close(progressCh)
stream, err := c.client.UpdateContainer(ctx, &pb.UpdateContainerRequest{ContainerId: containerID})
if err != nil {
return err
}
for {
progress, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
progressCh <- container.UpdateProgress{
Status: progress.Status,
Layer: progress.Layer,
Current: progress.Current,
Total: progress.Total,
Error: progress.Error,
}
}
}
func (c *Client) ContainerAttach(ctx context.Context, containerId string) (*container.ExecSession, error) {
stream, err := c.client.ContainerAttach(ctx)
if err != nil {
+5
View File
@@ -100,6 +100,11 @@ 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 {
args := m.Called(ctx, c, progressCh)
return args.Error(0)
}
var wantedContainer = container.Container{}
func init() {
+229 -95
View File
@@ -942,6 +942,126 @@ func (*ContainerActionResponse) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{19}
}
type UpdateContainerRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateContainerRequest) Reset() {
*x = UpdateContainerRequest{}
mi := &file_rpc_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateContainerRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateContainerRequest) ProtoMessage() {}
func (x *UpdateContainerRequest) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[20]
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 UpdateContainerRequest.ProtoReflect.Descriptor instead.
func (*UpdateContainerRequest) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{20}
}
func (x *UpdateContainerRequest) GetContainerId() string {
if x != nil {
return x.ContainerId
}
return ""
}
type UpdateContainerProgress struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
Layer string `protobuf:"bytes,2,opt,name=layer,proto3" json:"layer,omitempty"`
Current int64 `protobuf:"varint,3,opt,name=current,proto3" json:"current,omitempty"`
Total int64 `protobuf:"varint,4,opt,name=total,proto3" json:"total,omitempty"`
Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateContainerProgress) Reset() {
*x = UpdateContainerProgress{}
mi := &file_rpc_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateContainerProgress) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateContainerProgress) ProtoMessage() {}
func (x *UpdateContainerProgress) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[21]
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 UpdateContainerProgress.ProtoReflect.Descriptor instead.
func (*UpdateContainerProgress) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{21}
}
func (x *UpdateContainerProgress) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
func (x *UpdateContainerProgress) GetLayer() string {
if x != nil {
return x.Layer
}
return ""
}
func (x *UpdateContainerProgress) GetCurrent() int64 {
if x != nil {
return x.Current
}
return 0
}
func (x *UpdateContainerProgress) GetTotal() int64 {
if x != nil {
return x.Total
}
return 0
}
func (x *UpdateContainerProgress) GetError() string {
if x != nil {
return x.Error
}
return ""
}
type ContainerExecRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ContainerId string `protobuf:"bytes,1,opt,name=containerId,proto3" json:"containerId,omitempty"`
@@ -957,7 +1077,7 @@ type ContainerExecRequest struct {
func (x *ContainerExecRequest) Reset() {
*x = ContainerExecRequest{}
mi := &file_rpc_proto_msgTypes[20]
mi := &file_rpc_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -969,7 +1089,7 @@ func (x *ContainerExecRequest) String() string {
func (*ContainerExecRequest) ProtoMessage() {}
func (x *ContainerExecRequest) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[20]
mi := &file_rpc_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -982,7 +1102,7 @@ func (x *ContainerExecRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ContainerExecRequest.ProtoReflect.Descriptor instead.
func (*ContainerExecRequest) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{20}
return file_rpc_proto_rawDescGZIP(), []int{22}
}
func (x *ContainerExecRequest) GetContainerId() string {
@@ -1050,7 +1170,7 @@ type ResizePayload struct {
func (x *ResizePayload) Reset() {
*x = ResizePayload{}
mi := &file_rpc_proto_msgTypes[21]
mi := &file_rpc_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1062,7 +1182,7 @@ func (x *ResizePayload) String() string {
func (*ResizePayload) ProtoMessage() {}
func (x *ResizePayload) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[21]
mi := &file_rpc_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1075,7 +1195,7 @@ func (x *ResizePayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use ResizePayload.ProtoReflect.Descriptor instead.
func (*ResizePayload) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{21}
return file_rpc_proto_rawDescGZIP(), []int{23}
}
func (x *ResizePayload) GetWidth() uint32 {
@@ -1101,7 +1221,7 @@ type ContainerExecResponse struct {
func (x *ContainerExecResponse) Reset() {
*x = ContainerExecResponse{}
mi := &file_rpc_proto_msgTypes[22]
mi := &file_rpc_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1113,7 +1233,7 @@ func (x *ContainerExecResponse) String() string {
func (*ContainerExecResponse) ProtoMessage() {}
func (x *ContainerExecResponse) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[22]
mi := &file_rpc_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1126,7 +1246,7 @@ func (x *ContainerExecResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ContainerExecResponse.ProtoReflect.Descriptor instead.
func (*ContainerExecResponse) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{22}
return file_rpc_proto_rawDescGZIP(), []int{24}
}
func (x *ContainerExecResponse) GetStdout() []byte {
@@ -1150,7 +1270,7 @@ type ContainerAttachRequest struct {
func (x *ContainerAttachRequest) Reset() {
*x = ContainerAttachRequest{}
mi := &file_rpc_proto_msgTypes[23]
mi := &file_rpc_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1162,7 +1282,7 @@ func (x *ContainerAttachRequest) String() string {
func (*ContainerAttachRequest) ProtoMessage() {}
func (x *ContainerAttachRequest) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[23]
mi := &file_rpc_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1175,7 +1295,7 @@ func (x *ContainerAttachRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ContainerAttachRequest.ProtoReflect.Descriptor instead.
func (*ContainerAttachRequest) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{23}
return file_rpc_proto_rawDescGZIP(), []int{25}
}
func (x *ContainerAttachRequest) GetContainerId() string {
@@ -1235,7 +1355,7 @@ type ContainerAttachResponse struct {
func (x *ContainerAttachResponse) Reset() {
*x = ContainerAttachResponse{}
mi := &file_rpc_proto_msgTypes[24]
mi := &file_rpc_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1247,7 +1367,7 @@ func (x *ContainerAttachResponse) String() string {
func (*ContainerAttachResponse) ProtoMessage() {}
func (x *ContainerAttachResponse) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[24]
mi := &file_rpc_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1260,7 +1380,7 @@ func (x *ContainerAttachResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ContainerAttachResponse.ProtoReflect.Descriptor instead.
func (*ContainerAttachResponse) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{24}
return file_rpc_proto_rawDescGZIP(), []int{26}
}
func (x *ContainerAttachResponse) GetStdout() []byte {
@@ -1280,7 +1400,7 @@ type UpdateNotificationConfigRequest struct {
func (x *UpdateNotificationConfigRequest) Reset() {
*x = UpdateNotificationConfigRequest{}
mi := &file_rpc_proto_msgTypes[25]
mi := &file_rpc_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1292,7 +1412,7 @@ func (x *UpdateNotificationConfigRequest) String() string {
func (*UpdateNotificationConfigRequest) ProtoMessage() {}
func (x *UpdateNotificationConfigRequest) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[25]
mi := &file_rpc_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1305,7 +1425,7 @@ func (x *UpdateNotificationConfigRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateNotificationConfigRequest.ProtoReflect.Descriptor instead.
func (*UpdateNotificationConfigRequest) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{25}
return file_rpc_proto_rawDescGZIP(), []int{27}
}
func (x *UpdateNotificationConfigRequest) GetSubscriptions() []*NotificationSubscription {
@@ -1330,7 +1450,7 @@ type UpdateNotificationConfigResponse struct {
func (x *UpdateNotificationConfigResponse) Reset() {
*x = UpdateNotificationConfigResponse{}
mi := &file_rpc_proto_msgTypes[26]
mi := &file_rpc_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1342,7 +1462,7 @@ func (x *UpdateNotificationConfigResponse) String() string {
func (*UpdateNotificationConfigResponse) ProtoMessage() {}
func (x *UpdateNotificationConfigResponse) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[26]
mi := &file_rpc_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1355,7 +1475,7 @@ func (x *UpdateNotificationConfigResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateNotificationConfigResponse.ProtoReflect.Descriptor instead.
func (*UpdateNotificationConfigResponse) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{26}
return file_rpc_proto_rawDescGZIP(), []int{28}
}
type GetNotificationStatsRequest struct {
@@ -1366,7 +1486,7 @@ type GetNotificationStatsRequest struct {
func (x *GetNotificationStatsRequest) Reset() {
*x = GetNotificationStatsRequest{}
mi := &file_rpc_proto_msgTypes[27]
mi := &file_rpc_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1378,7 +1498,7 @@ func (x *GetNotificationStatsRequest) String() string {
func (*GetNotificationStatsRequest) ProtoMessage() {}
func (x *GetNotificationStatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[27]
mi := &file_rpc_proto_msgTypes[29]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1391,7 +1511,7 @@ func (x *GetNotificationStatsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetNotificationStatsRequest.ProtoReflect.Descriptor instead.
func (*GetNotificationStatsRequest) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{27}
return file_rpc_proto_rawDescGZIP(), []int{29}
}
type GetNotificationStatsResponse struct {
@@ -1403,7 +1523,7 @@ type GetNotificationStatsResponse struct {
func (x *GetNotificationStatsResponse) Reset() {
*x = GetNotificationStatsResponse{}
mi := &file_rpc_proto_msgTypes[28]
mi := &file_rpc_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1415,7 +1535,7 @@ func (x *GetNotificationStatsResponse) String() string {
func (*GetNotificationStatsResponse) ProtoMessage() {}
func (x *GetNotificationStatsResponse) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[28]
mi := &file_rpc_proto_msgTypes[30]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1428,7 +1548,7 @@ func (x *GetNotificationStatsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetNotificationStatsResponse.ProtoReflect.Descriptor instead.
func (*GetNotificationStatsResponse) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{28}
return file_rpc_proto_rawDescGZIP(), []int{30}
}
func (x *GetNotificationStatsResponse) GetStats() []*NotificationSubscriptionStats {
@@ -1495,7 +1615,15 @@ const file_rpc_proto_rawDesc = "" +
"\x16ContainerActionRequest\x12 \n" +
"\vcontainerId\x18\x01 \x01(\tR\vcontainerId\x121\n" +
"\x06action\x18\x02 \x01(\x0e2\x19.protobuf.ContainerActionR\x06action\"\x19\n" +
"\x17ContainerActionResponse\"\xa8\x01\n" +
"\x17ContainerActionResponse\":\n" +
"\x16UpdateContainerRequest\x12 \n" +
"\vcontainerId\x18\x01 \x01(\tR\vcontainerId\"\x8d\x01\n" +
"\x17UpdateContainerProgress\x12\x16\n" +
"\x06status\x18\x01 \x01(\tR\x06status\x12\x14\n" +
"\x05layer\x18\x02 \x01(\tR\x05layer\x12\x18\n" +
"\acurrent\x18\x03 \x01(\x03R\acurrent\x12\x14\n" +
"\x05total\x18\x04 \x01(\x03R\x05total\x12\x14\n" +
"\x05error\x18\x05 \x01(\tR\x05error\"\xa8\x01\n" +
"\x14ContainerExecRequest\x12 \n" +
"\vcontainerId\x18\x01 \x01(\tR\vcontainerId\x12\x18\n" +
"\acommand\x18\x02 \x03(\tR\acommand\x12\x16\n" +
@@ -1520,7 +1648,8 @@ const file_rpc_proto_rawDesc = "" +
" UpdateNotificationConfigResponse\"\x1d\n" +
"\x1bGetNotificationStatsRequest\"]\n" +
"\x1cGetNotificationStatsResponse\x12=\n" +
"\x05stats\x18\x01 \x03(\v2'.protobuf.NotificationSubscriptionStatsR\x05stats2\xff\t\n" +
"\x05stats\x18\x01 \x03(\v2'.protobuf.NotificationSubscriptionStatsR\x05stats2\xdb\n" +
"\n" +
"\fAgentService\x12U\n" +
"\x0eListContainers\x12\x1f.protobuf.ListContainersRequest\x1a .protobuf.ListContainersResponse\"\x00\x12R\n" +
"\rFindContainer\x12\x1e.protobuf.FindContainerRequest\x1a\x1f.protobuf.FindContainerResponse\"\x00\x12K\n" +
@@ -1532,7 +1661,8 @@ const file_rpc_proto_rawDesc = "" +
"\vStreamStats\x12\x1c.protobuf.StreamStatsRequest\x1a\x1d.protobuf.StreamStatsResponse\"\x000\x01\x12o\n" +
"\x16StreamContainerStarted\x12'.protobuf.StreamContainerStartedRequest\x1a(.protobuf.StreamContainerStartedResponse\"\x000\x01\x12C\n" +
"\bHostInfo\x12\x19.protobuf.HostInfoRequest\x1a\x1a.protobuf.HostInfoResponse\"\x00\x12X\n" +
"\x0fContainerAction\x12 .protobuf.ContainerActionRequest\x1a!.protobuf.ContainerActionResponse\"\x00\x12V\n" +
"\x0fContainerAction\x12 .protobuf.ContainerActionRequest\x1a!.protobuf.ContainerActionResponse\"\x00\x12Z\n" +
"\x0fUpdateContainer\x12 .protobuf.UpdateContainerRequest\x1a!.protobuf.UpdateContainerProgress\"\x000\x01\x12V\n" +
"\rContainerExec\x12\x1e.protobuf.ContainerExecRequest\x1a\x1f.protobuf.ContainerExecResponse\"\x00(\x010\x01\x12\\\n" +
"\x0fContainerAttach\x12 .protobuf.ContainerAttachRequest\x1a!.protobuf.ContainerAttachResponse\"\x00(\x010\x01\x12s\n" +
"\x18UpdateNotificationConfig\x12).protobuf.UpdateNotificationConfigRequest\x1a*.protobuf.UpdateNotificationConfigResponse\"\x00\x12g\n" +
@@ -1550,7 +1680,7 @@ func file_rpc_proto_rawDescGZIP() []byte {
return file_rpc_proto_rawDescData
}
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 31)
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 33)
var file_rpc_proto_goTypes = []any{
(*ListContainersRequest)(nil), // 0: protobuf.ListContainersRequest
(*RepeatedString)(nil), // 1: protobuf.RepeatedString
@@ -1572,49 +1702,51 @@ var file_rpc_proto_goTypes = []any{
(*StreamContainerStartedResponse)(nil), // 17: protobuf.StreamContainerStartedResponse
(*ContainerActionRequest)(nil), // 18: protobuf.ContainerActionRequest
(*ContainerActionResponse)(nil), // 19: protobuf.ContainerActionResponse
(*ContainerExecRequest)(nil), // 20: protobuf.ContainerExecRequest
(*ResizePayload)(nil), // 21: protobuf.ResizePayload
(*ContainerExecResponse)(nil), // 22: protobuf.ContainerExecResponse
(*ContainerAttachRequest)(nil), // 23: protobuf.ContainerAttachRequest
(*ContainerAttachResponse)(nil), // 24: protobuf.ContainerAttachResponse
(*UpdateNotificationConfigRequest)(nil), // 25: protobuf.UpdateNotificationConfigRequest
(*UpdateNotificationConfigResponse)(nil), // 26: protobuf.UpdateNotificationConfigResponse
(*GetNotificationStatsRequest)(nil), // 27: protobuf.GetNotificationStatsRequest
(*GetNotificationStatsResponse)(nil), // 28: protobuf.GetNotificationStatsResponse
nil, // 29: protobuf.ListContainersRequest.FilterEntry
nil, // 30: protobuf.FindContainerRequest.FilterEntry
(*Container)(nil), // 31: protobuf.Container
(*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp
(*LogEvent)(nil), // 33: protobuf.LogEvent
(*ContainerEvent)(nil), // 34: protobuf.ContainerEvent
(*ContainerStat)(nil), // 35: protobuf.ContainerStat
(*Host)(nil), // 36: protobuf.Host
(ContainerAction)(0), // 37: protobuf.ContainerAction
(*NotificationSubscription)(nil), // 38: protobuf.NotificationSubscription
(*NotificationDispatcher)(nil), // 39: protobuf.NotificationDispatcher
(*NotificationSubscriptionStats)(nil), // 40: protobuf.NotificationSubscriptionStats
(*UpdateContainerRequest)(nil), // 20: protobuf.UpdateContainerRequest
(*UpdateContainerProgress)(nil), // 21: protobuf.UpdateContainerProgress
(*ContainerExecRequest)(nil), // 22: protobuf.ContainerExecRequest
(*ResizePayload)(nil), // 23: protobuf.ResizePayload
(*ContainerExecResponse)(nil), // 24: protobuf.ContainerExecResponse
(*ContainerAttachRequest)(nil), // 25: protobuf.ContainerAttachRequest
(*ContainerAttachResponse)(nil), // 26: protobuf.ContainerAttachResponse
(*UpdateNotificationConfigRequest)(nil), // 27: protobuf.UpdateNotificationConfigRequest
(*UpdateNotificationConfigResponse)(nil), // 28: protobuf.UpdateNotificationConfigResponse
(*GetNotificationStatsRequest)(nil), // 29: protobuf.GetNotificationStatsRequest
(*GetNotificationStatsResponse)(nil), // 30: protobuf.GetNotificationStatsResponse
nil, // 31: protobuf.ListContainersRequest.FilterEntry
nil, // 32: protobuf.FindContainerRequest.FilterEntry
(*Container)(nil), // 33: protobuf.Container
(*timestamppb.Timestamp)(nil), // 34: google.protobuf.Timestamp
(*LogEvent)(nil), // 35: protobuf.LogEvent
(*ContainerEvent)(nil), // 36: protobuf.ContainerEvent
(*ContainerStat)(nil), // 37: protobuf.ContainerStat
(*Host)(nil), // 38: protobuf.Host
(ContainerAction)(0), // 39: protobuf.ContainerAction
(*NotificationSubscription)(nil), // 40: protobuf.NotificationSubscription
(*NotificationDispatcher)(nil), // 41: protobuf.NotificationDispatcher
(*NotificationSubscriptionStats)(nil), // 42: protobuf.NotificationSubscriptionStats
}
var file_rpc_proto_depIdxs = []int32{
29, // 0: protobuf.ListContainersRequest.filter:type_name -> protobuf.ListContainersRequest.FilterEntry
31, // 1: protobuf.ListContainersResponse.containers:type_name -> protobuf.Container
30, // 2: protobuf.FindContainerRequest.filter:type_name -> protobuf.FindContainerRequest.FilterEntry
31, // 3: protobuf.FindContainerResponse.container:type_name -> protobuf.Container
32, // 4: protobuf.StreamLogsRequest.since:type_name -> google.protobuf.Timestamp
33, // 5: protobuf.StreamLogsResponse.event:type_name -> protobuf.LogEvent
32, // 6: protobuf.LogsBetweenDatesRequest.since:type_name -> google.protobuf.Timestamp
32, // 7: protobuf.LogsBetweenDatesRequest.until:type_name -> google.protobuf.Timestamp
32, // 8: protobuf.StreamRawBytesRequest.since:type_name -> google.protobuf.Timestamp
32, // 9: protobuf.StreamRawBytesRequest.until:type_name -> google.protobuf.Timestamp
34, // 10: protobuf.StreamEventsResponse.event:type_name -> protobuf.ContainerEvent
35, // 11: protobuf.StreamStatsResponse.stat:type_name -> protobuf.ContainerStat
36, // 12: protobuf.HostInfoResponse.host:type_name -> protobuf.Host
31, // 13: protobuf.StreamContainerStartedResponse.container:type_name -> protobuf.Container
37, // 14: protobuf.ContainerActionRequest.action:type_name -> protobuf.ContainerAction
21, // 15: protobuf.ContainerExecRequest.resize:type_name -> protobuf.ResizePayload
21, // 16: protobuf.ContainerAttachRequest.resize:type_name -> protobuf.ResizePayload
38, // 17: protobuf.UpdateNotificationConfigRequest.subscriptions:type_name -> protobuf.NotificationSubscription
39, // 18: protobuf.UpdateNotificationConfigRequest.dispatchers:type_name -> protobuf.NotificationDispatcher
40, // 19: protobuf.GetNotificationStatsResponse.stats:type_name -> protobuf.NotificationSubscriptionStats
31, // 0: protobuf.ListContainersRequest.filter:type_name -> protobuf.ListContainersRequest.FilterEntry
33, // 1: protobuf.ListContainersResponse.containers:type_name -> protobuf.Container
32, // 2: protobuf.FindContainerRequest.filter:type_name -> protobuf.FindContainerRequest.FilterEntry
33, // 3: protobuf.FindContainerResponse.container:type_name -> protobuf.Container
34, // 4: protobuf.StreamLogsRequest.since:type_name -> google.protobuf.Timestamp
35, // 5: protobuf.StreamLogsResponse.event:type_name -> protobuf.LogEvent
34, // 6: protobuf.LogsBetweenDatesRequest.since:type_name -> google.protobuf.Timestamp
34, // 7: protobuf.LogsBetweenDatesRequest.until:type_name -> google.protobuf.Timestamp
34, // 8: protobuf.StreamRawBytesRequest.since:type_name -> google.protobuf.Timestamp
34, // 9: protobuf.StreamRawBytesRequest.until:type_name -> google.protobuf.Timestamp
36, // 10: protobuf.StreamEventsResponse.event:type_name -> protobuf.ContainerEvent
37, // 11: protobuf.StreamStatsResponse.stat:type_name -> protobuf.ContainerStat
38, // 12: protobuf.HostInfoResponse.host:type_name -> protobuf.Host
33, // 13: protobuf.StreamContainerStartedResponse.container:type_name -> protobuf.Container
39, // 14: protobuf.ContainerActionRequest.action:type_name -> protobuf.ContainerAction
23, // 15: protobuf.ContainerExecRequest.resize:type_name -> protobuf.ResizePayload
23, // 16: protobuf.ContainerAttachRequest.resize:type_name -> protobuf.ResizePayload
40, // 17: protobuf.UpdateNotificationConfigRequest.subscriptions:type_name -> protobuf.NotificationSubscription
41, // 18: protobuf.UpdateNotificationConfigRequest.dispatchers:type_name -> protobuf.NotificationDispatcher
42, // 19: protobuf.GetNotificationStatsResponse.stats:type_name -> protobuf.NotificationSubscriptionStats
1, // 20: protobuf.ListContainersRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
1, // 21: protobuf.FindContainerRequest.FilterEntry.value:type_name -> protobuf.RepeatedString
0, // 22: protobuf.AgentService.ListContainers:input_type -> protobuf.ListContainersRequest
@@ -1627,26 +1759,28 @@ var file_rpc_proto_depIdxs = []int32{
16, // 29: protobuf.AgentService.StreamContainerStarted:input_type -> protobuf.StreamContainerStartedRequest
14, // 30: protobuf.AgentService.HostInfo:input_type -> protobuf.HostInfoRequest
18, // 31: protobuf.AgentService.ContainerAction:input_type -> protobuf.ContainerActionRequest
20, // 32: protobuf.AgentService.ContainerExec:input_type -> protobuf.ContainerExecRequest
23, // 33: protobuf.AgentService.ContainerAttach:input_type -> protobuf.ContainerAttachRequest
25, // 34: protobuf.AgentService.UpdateNotificationConfig:input_type -> protobuf.UpdateNotificationConfigRequest
27, // 35: protobuf.AgentService.GetNotificationStats:input_type -> protobuf.GetNotificationStatsRequest
2, // 36: protobuf.AgentService.ListContainers:output_type -> protobuf.ListContainersResponse
4, // 37: protobuf.AgentService.FindContainer:output_type -> protobuf.FindContainerResponse
6, // 38: protobuf.AgentService.StreamLogs:output_type -> protobuf.StreamLogsResponse
6, // 39: protobuf.AgentService.LogsBetweenDates:output_type -> protobuf.StreamLogsResponse
9, // 40: protobuf.AgentService.StreamRawBytes:output_type -> protobuf.StreamRawBytesResponse
11, // 41: protobuf.AgentService.StreamEvents:output_type -> protobuf.StreamEventsResponse
13, // 42: protobuf.AgentService.StreamStats:output_type -> protobuf.StreamStatsResponse
17, // 43: protobuf.AgentService.StreamContainerStarted:output_type -> protobuf.StreamContainerStartedResponse
15, // 44: protobuf.AgentService.HostInfo:output_type -> protobuf.HostInfoResponse
19, // 45: protobuf.AgentService.ContainerAction:output_type -> protobuf.ContainerActionResponse
22, // 46: protobuf.AgentService.ContainerExec:output_type -> protobuf.ContainerExecResponse
24, // 47: protobuf.AgentService.ContainerAttach:output_type -> protobuf.ContainerAttachResponse
26, // 48: protobuf.AgentService.UpdateNotificationConfig:output_type -> protobuf.UpdateNotificationConfigResponse
28, // 49: protobuf.AgentService.GetNotificationStats:output_type -> protobuf.GetNotificationStatsResponse
36, // [36:50] is the sub-list for method output_type
22, // [22:36] is the sub-list for method input_type
20, // 32: protobuf.AgentService.UpdateContainer:input_type -> protobuf.UpdateContainerRequest
22, // 33: protobuf.AgentService.ContainerExec:input_type -> protobuf.ContainerExecRequest
25, // 34: protobuf.AgentService.ContainerAttach:input_type -> protobuf.ContainerAttachRequest
27, // 35: protobuf.AgentService.UpdateNotificationConfig:input_type -> protobuf.UpdateNotificationConfigRequest
29, // 36: protobuf.AgentService.GetNotificationStats:input_type -> protobuf.GetNotificationStatsRequest
2, // 37: protobuf.AgentService.ListContainers:output_type -> protobuf.ListContainersResponse
4, // 38: protobuf.AgentService.FindContainer:output_type -> protobuf.FindContainerResponse
6, // 39: protobuf.AgentService.StreamLogs:output_type -> protobuf.StreamLogsResponse
6, // 40: protobuf.AgentService.LogsBetweenDates:output_type -> protobuf.StreamLogsResponse
9, // 41: protobuf.AgentService.StreamRawBytes:output_type -> protobuf.StreamRawBytesResponse
11, // 42: protobuf.AgentService.StreamEvents:output_type -> protobuf.StreamEventsResponse
13, // 43: protobuf.AgentService.StreamStats:output_type -> protobuf.StreamStatsResponse
17, // 44: protobuf.AgentService.StreamContainerStarted:output_type -> protobuf.StreamContainerStartedResponse
15, // 45: protobuf.AgentService.HostInfo:output_type -> protobuf.HostInfoResponse
19, // 46: protobuf.AgentService.ContainerAction:output_type -> protobuf.ContainerActionResponse
21, // 47: protobuf.AgentService.UpdateContainer:output_type -> protobuf.UpdateContainerProgress
24, // 48: protobuf.AgentService.ContainerExec:output_type -> protobuf.ContainerExecResponse
26, // 49: protobuf.AgentService.ContainerAttach:output_type -> protobuf.ContainerAttachResponse
28, // 50: protobuf.AgentService.UpdateNotificationConfig:output_type -> protobuf.UpdateNotificationConfigResponse
30, // 51: protobuf.AgentService.GetNotificationStats:output_type -> protobuf.GetNotificationStatsResponse
37, // [37:52] is the sub-list for method output_type
22, // [22:37] is the sub-list for method input_type
22, // [22:22] is the sub-list for extension type_name
22, // [22:22] is the sub-list for extension extendee
0, // [0:22] is the sub-list for field type_name
@@ -1658,11 +1792,11 @@ func file_rpc_proto_init() {
return
}
file_types_proto_init()
file_rpc_proto_msgTypes[20].OneofWrappers = []any{
file_rpc_proto_msgTypes[22].OneofWrappers = []any{
(*ContainerExecRequest_Stdin)(nil),
(*ContainerExecRequest_Resize)(nil),
}
file_rpc_proto_msgTypes[23].OneofWrappers = []any{
file_rpc_proto_msgTypes[25].OneofWrappers = []any{
(*ContainerAttachRequest_Stdin)(nil),
(*ContainerAttachRequest_Resize)(nil),
}
@@ -1672,7 +1806,7 @@ func file_rpc_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_rpc_proto_rawDesc), len(file_rpc_proto_rawDesc)),
NumEnums: 0,
NumMessages: 31,
NumMessages: 33,
NumExtensions: 0,
NumServices: 1,
},
+43 -2
View File
@@ -29,6 +29,7 @@ const (
AgentService_StreamContainerStarted_FullMethodName = "/protobuf.AgentService/StreamContainerStarted"
AgentService_HostInfo_FullMethodName = "/protobuf.AgentService/HostInfo"
AgentService_ContainerAction_FullMethodName = "/protobuf.AgentService/ContainerAction"
AgentService_UpdateContainer_FullMethodName = "/protobuf.AgentService/UpdateContainer"
AgentService_ContainerExec_FullMethodName = "/protobuf.AgentService/ContainerExec"
AgentService_ContainerAttach_FullMethodName = "/protobuf.AgentService/ContainerAttach"
AgentService_UpdateNotificationConfig_FullMethodName = "/protobuf.AgentService/UpdateNotificationConfig"
@@ -49,6 +50,7 @@ type AgentServiceClient interface {
StreamContainerStarted(ctx context.Context, in *StreamContainerStartedRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StreamContainerStartedResponse], error)
HostInfo(ctx context.Context, in *HostInfoRequest, opts ...grpc.CallOption) (*HostInfoResponse, error)
ContainerAction(ctx context.Context, in *ContainerActionRequest, opts ...grpc.CallOption) (*ContainerActionResponse, error)
UpdateContainer(ctx context.Context, in *UpdateContainerRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[UpdateContainerProgress], error)
ContainerExec(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ContainerExecRequest, ContainerExecResponse], error)
ContainerAttach(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ContainerAttachRequest, ContainerAttachResponse], error)
UpdateNotificationConfig(ctx context.Context, in *UpdateNotificationConfigRequest, opts ...grpc.CallOption) (*UpdateNotificationConfigResponse, error)
@@ -217,9 +219,28 @@ func (c *agentServiceClient) ContainerAction(ctx context.Context, in *ContainerA
return out, nil
}
func (c *agentServiceClient) UpdateContainer(ctx context.Context, in *UpdateContainerRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[UpdateContainerProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &AgentService_ServiceDesc.Streams[6], AgentService_UpdateContainer_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[UpdateContainerRequest, UpdateContainerProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type AgentService_UpdateContainerClient = grpc.ServerStreamingClient[UpdateContainerProgress]
func (c *agentServiceClient) ContainerExec(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ContainerExecRequest, ContainerExecResponse], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &AgentService_ServiceDesc.Streams[6], AgentService_ContainerExec_FullMethodName, cOpts...)
stream, err := c.cc.NewStream(ctx, &AgentService_ServiceDesc.Streams[7], AgentService_ContainerExec_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
@@ -232,7 +253,7 @@ type AgentService_ContainerExecClient = grpc.BidiStreamingClient[ContainerExecRe
func (c *agentServiceClient) ContainerAttach(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ContainerAttachRequest, ContainerAttachResponse], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &AgentService_ServiceDesc.Streams[7], AgentService_ContainerAttach_FullMethodName, cOpts...)
stream, err := c.cc.NewStream(ctx, &AgentService_ServiceDesc.Streams[8], AgentService_ContainerAttach_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
@@ -277,6 +298,7 @@ type AgentServiceServer interface {
StreamContainerStarted(*StreamContainerStartedRequest, grpc.ServerStreamingServer[StreamContainerStartedResponse]) error
HostInfo(context.Context, *HostInfoRequest) (*HostInfoResponse, error)
ContainerAction(context.Context, *ContainerActionRequest) (*ContainerActionResponse, error)
UpdateContainer(*UpdateContainerRequest, grpc.ServerStreamingServer[UpdateContainerProgress]) error
ContainerExec(grpc.BidiStreamingServer[ContainerExecRequest, ContainerExecResponse]) error
ContainerAttach(grpc.BidiStreamingServer[ContainerAttachRequest, ContainerAttachResponse]) error
UpdateNotificationConfig(context.Context, *UpdateNotificationConfigRequest) (*UpdateNotificationConfigResponse, error)
@@ -321,6 +343,9 @@ func (UnimplementedAgentServiceServer) HostInfo(context.Context, *HostInfoReques
func (UnimplementedAgentServiceServer) ContainerAction(context.Context, *ContainerActionRequest) (*ContainerActionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ContainerAction not implemented")
}
func (UnimplementedAgentServiceServer) UpdateContainer(*UpdateContainerRequest, grpc.ServerStreamingServer[UpdateContainerProgress]) error {
return status.Error(codes.Unimplemented, "method UpdateContainer not implemented")
}
func (UnimplementedAgentServiceServer) ContainerExec(grpc.BidiStreamingServer[ContainerExecRequest, ContainerExecResponse]) error {
return status.Error(codes.Unimplemented, "method ContainerExec not implemented")
}
@@ -492,6 +517,17 @@ func _AgentService_ContainerAction_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler)
}
func _AgentService_UpdateContainer_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(UpdateContainerRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(AgentServiceServer).UpdateContainer(m, &grpc.GenericServerStream[UpdateContainerRequest, UpdateContainerProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type AgentService_UpdateContainerServer = grpc.ServerStreamingServer[UpdateContainerProgress]
func _AgentService_ContainerExec_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(AgentServiceServer).ContainerExec(&grpc.GenericServerStream[ContainerExecRequest, ContainerExecResponse]{ServerStream: stream})
}
@@ -605,6 +641,11 @@ var AgentService_ServiceDesc = grpc.ServiceDesc{
Handler: _AgentService_StreamContainerStarted_Handler,
ServerStreams: true,
},
{
StreamName: "UpdateContainer",
Handler: _AgentService_UpdateContainer_Handler,
ServerStreams: true,
},
{
StreamName: "ContainerExec",
Handler: _AgentService_ContainerExec_Handler,
+29
View File
@@ -39,6 +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
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)
@@ -313,6 +314,34 @@ func (s *server) ContainerAction(ctx context.Context, in *pb.ContainerActionRequ
return &pb.ContainerActionResponse{}, nil
}
func (s *server) UpdateContainer(req *pb.UpdateContainerRequest, out pb.AgentService_UpdateContainerServer) error {
c, err := s.service.FindContainer(out.Context(), req.ContainerId, container.ContainerLabels{})
if err != nil {
return status.Error(codes.NotFound, err.Error())
}
progressCh := make(chan container.UpdateProgress)
errCh := make(chan error, 1)
go func() {
errCh <- s.service.UpdateContainer(out.Context(), c, progressCh)
}()
for progress := range progressCh {
if err := out.Send(&pb.UpdateContainerProgress{
Status: progress.Status,
Layer: progress.Layer,
Current: progress.Current,
Total: progress.Total,
Error: progress.Error,
}); err != nil {
return err
}
}
return <-errCh
}
// terminalMessage represents a message from a terminal gRPC stream (exec or attach)
type terminalMessage interface {
GetStdin() []byte
+26 -6
View File
@@ -25,11 +25,12 @@ import (
)
const (
initialBackoff = 1 * time.Second
maxBackoff = 30 * time.Second
backoffFactor = 2
jitterFraction = 0.1
maxConcurrent = 5
initialBackoff = 1 * time.Second
maxBackoff = 30 * time.Second
backoffFactor = 2
jitterFraction = 0.1
maxConcurrent = 5
unauthenticatedPause = 1 * time.Hour
)
// Client manages the gRPC connection to Dozzle Cloud
@@ -134,6 +135,17 @@ func (c *Client) Run(ctx context.Context) {
log.Debug().Msg("cloud account does not have pro plan, stopping cloud client")
return
}
if isUnauthenticated(err) {
log.Warn().Err(err).Dur("pause", unauthenticatedPause).Msg("invalid API key, pausing before retry")
backoffTimer.Reset(unauthenticatedPause)
select {
case <-ctx.Done():
return
case <-backoffTimer.C:
}
backoff = initialBackoff
continue
}
log.Warn().Err(err).Dur("backoff", backoff).Msg("cloud connection failed, reconnecting")
}
@@ -289,8 +301,16 @@ func (c *Client) tools() []*pb.ToolDefinition {
}
func isPermissionDenied(err error) bool {
return hasGRPCCode(err, codes.PermissionDenied)
}
func isUnauthenticated(err error) bool {
return hasGRPCCode(err, codes.Unauthenticated)
}
func hasGRPCCode(err error, code codes.Code) bool {
for e := err; e != nil; e = errors.Unwrap(e) {
if s, ok := status.FromError(e); ok && s.Code() == codes.PermissionDenied {
if s, ok := status.FromError(e); ok && s.Code() == code {
return true
}
}
+1 -1
View File
@@ -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.Tools, 10) // list_hosts + find_containers + list_running/all + get_stats + fetch_logs + inspect_container + 3 actions
assert.Len(t, listResp.Tools, 11) // list_hosts + find_containers + list_running/all + get_stats + fetch_logs + inspect_container + 3 actions + update
}
func TestHandleRequest_ListTools_ActionsDisabled(t *testing.T) {
+10
View File
@@ -80,6 +80,11 @@ func AvailableTools(enableActions bool) []*pb.ToolDefinition {
Description: "Restart a Docker container",
ParametersJson: actionParams,
},
&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,
},
)
}
@@ -124,6 +129,11 @@ func executeTool(ctx context.Context, name string, argsJSON string, enableAction
return nil, fmt.Errorf("container actions are not enabled")
}
return executeContainerAction(ctx, name, argsJSON, hostService, labels)
case "update_container":
if !enableActions {
return nil, fmt.Errorf("container actions are not enabled")
}
return executeUpdateContainer(ctx, argsJSON, hostService, labels)
default:
return nil, fmt.Errorf("unknown tool: %s", name)
}
+57
View File
@@ -51,6 +51,63 @@ func executeContainerAction(ctx context.Context, name string, argsJSON string, h
}, nil
}
func executeUpdateContainer(ctx context.Context, argsJSON string, hostService ToolHostService, labels container.ContainerLabels) (*pb.CallToolResponse, error) {
var args containerActionArgs
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
return nil, fmt.Errorf("failed to parse arguments: %w", err)
}
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)
if err != nil {
return nil, fmt.Errorf("container not found: %w", err)
}
progressCh := make(chan container.UpdateProgress, 100)
errCh := make(chan error, 1)
go func() {
errCh <- cs.Update(ctx, progressCh)
}()
// 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
}
}
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)
}
return &pb.CallToolResponse{
Success: true,
Result: &pb.CallToolResponse_Action{Action: &pb.ActionResult{
Success: true,
ContainerId: args.ContainerID,
Action: action,
}},
}, nil
}
func resolveAction(name string) (container.ContainerAction, error) {
switch name {
case "start_container":
+6 -1
View File
@@ -30,7 +30,7 @@ func TestAvailableTools_WithActionsEnabled(t *testing.T) {
assert.Contains(t, names, "start_container")
assert.Contains(t, names, "stop_container")
assert.Contains(t, names, "restart_container")
assert.Len(t, tools, 10)
assert.Len(t, tools, 11)
}
func TestAvailableTools_WithActionsDisabled(t *testing.T) {
@@ -126,6 +126,11 @@ func (m *MockClientService) Exec(_ context.Context, _ container.Container, _ []s
return nil
}
func (m *MockClientService) UpdateContainer(_ context.Context, _ container.Container, progressCh chan<- container.UpdateProgress) error {
close(progressCh)
return nil
}
func TestExecuteTool_ListRunningContainers(t *testing.T) {
mockHost := &MockHostService{}
mockHost.On("ListAllContainers", container.ContainerLabels(nil)).Return([]container.Container{
+8
View File
@@ -204,6 +204,14 @@ func ParseContainerAction(input string) (ContainerAction, error) {
}
}
type UpdateProgress struct {
Status string `json:"status"` // "pulling", "recreating", "done", "error", "up-to-date"
Layer string `json:"layer"` // Docker layer ID (pull events only)
Current int64 `json:"current"` // Bytes downloaded
Total int64 `json:"total"` // Total bytes for layer
Error string `json:"error"` // Only when Status="error"
}
type LogEvent struct {
Type LogType `json:"t,omitempty"`
Message any `json:"m,omitempty"`
+60
View File
@@ -17,8 +17,12 @@ import (
docker "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/rs/zerolog/log"
)
@@ -38,6 +42,11 @@ type DockerCLI interface {
ContainerExecAttach(ctx context.Context, execID string, config docker.ExecAttachOptions) (types.HijackedResponse, error)
ContainerExecResize(ctx context.Context, execID string, options docker.ResizeOptions) error
Info(ctx context.Context) (system.Info, error)
ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error)
ContainerRemove(ctx context.Context, containerID string, options docker.RemoveOptions) error
ContainerCreate(ctx context.Context, config *docker.Config, hostConfig *docker.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (docker.CreateResponse, error)
ServiceInspectWithRaw(ctx context.Context, serviceID string, opts swarm.ServiceInspectOptions) (swarm.Service, []byte, error)
ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, opts swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error)
}
type DockerClient struct {
@@ -149,6 +158,57 @@ func (d *DockerClient) ContainerActions(ctx context.Context, action container.Co
}
}
func (d *DockerClient) ImagePull(ctx context.Context, imageName string) (io.ReadCloser, error) {
return d.cli.ImagePull(ctx, imageName, image.PullOptions{})
}
func (d *DockerClient) ContainerInspect(ctx context.Context, containerID string) (docker.InspectResponse, error) {
return d.cli.ContainerInspect(ctx, containerID)
}
func (d *DockerClient) ContainerRemove(ctx context.Context, containerID string) error {
return d.cli.ContainerRemove(ctx, containerID, docker.RemoveOptions{})
}
func (d *DockerClient) ContainerCreate(ctx context.Context, inspectResp docker.InspectResponse, name string) (string, error) {
// Build clean EndpointsConfig with only network names and aliases,
// stripping runtime state (IPs, gateways, MAC addresses) that can
// cause conflicts when recreating.
var networkingConfig *network.NetworkingConfig
if inspectResp.NetworkSettings != nil && len(inspectResp.NetworkSettings.Networks) > 0 {
endpointsConfig := make(map[string]*network.EndpointSettings, len(inspectResp.NetworkSettings.Networks))
for netName, ep := range inspectResp.NetworkSettings.Networks {
endpointsConfig[netName] = &network.EndpointSettings{
Aliases: ep.Aliases,
}
}
networkingConfig = &network.NetworkingConfig{EndpointsConfig: endpointsConfig}
}
resp, err := d.cli.ContainerCreate(ctx,
inspectResp.Config,
inspectResp.HostConfig,
networkingConfig,
nil,
name,
)
if err != nil {
return "", err
}
return resp.ID, nil
}
func (d *DockerClient) ServiceUpdate(ctx context.Context, serviceID string, imageName string) error {
svc, _, err := d.cli.ServiceInspectWithRaw(ctx, serviceID, swarm.ServiceInspectOptions{})
if err != nil {
return err
}
svc.Spec.TaskTemplate.ContainerSpec.Image = imageName
svc.Spec.TaskTemplate.ForceUpdate++
_, err = d.cli.ServiceUpdate(ctx, serviceID, svc.Version, svc.Spec, swarm.ServiceUpdateOptions{})
return err
}
func (d *DockerClient) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
log.Debug().Interface("labels", labels).Str("host", d.host.Name).Msg("Listing containers")
filterArgs := filters.NewArgs()
@@ -76,6 +76,10 @@ 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 {
return a.client.UpdateContainer(ctx, c.ID, progressCh)
}
func (a *agentService) Attach(ctx context.Context, c container.Container, events container.ExecEventReader, stdout io.Writer) error {
panic("not implemented")
}
@@ -15,6 +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
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,6 +36,10 @@ 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 {
return c.clientService.UpdateContainer(ctx, c.Container, progressCh)
}
func (c *ContainerService) Attach(ctx context.Context, events container.ExecEventReader, stdout io.Writer) error {
return c.clientService.Attach(ctx, c.Container, events, stdout)
}
+123 -2
View File
@@ -2,23 +2,37 @@ package docker_support
import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/amir20/dozzle/internal/container"
"github.com/amir20/dozzle/internal/docker"
docker_types "github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/rs/zerolog/log"
)
// DockerUpdateClient extends container.Client with Docker-specific update operations.
type DockerUpdateClient interface {
container.Client
ImagePull(ctx context.Context, image string) (io.ReadCloser, error)
ContainerInspect(ctx context.Context, containerID string) (docker_types.InspectResponse, error)
ContainerRemove(ctx context.Context, containerID string) error
ContainerCreate(ctx context.Context, inspectResp docker_types.InspectResponse, name string) (string, error)
ServiceUpdate(ctx context.Context, serviceID string, image string) error
}
type DockerClientService struct {
client container.Client
client DockerUpdateClient
store *container.ContainerStore
}
func NewDockerClientService(client container.Client, labels container.ContainerLabels) *DockerClientService {
func NewDockerClientService(client DockerUpdateClient, labels container.ContainerLabels) *DockerClientService {
statsCollector := docker.NewDockerStatsCollector(client, labels)
return &DockerClientService{
client: client,
@@ -91,6 +105,113 @@ func (d *DockerClientService) ContainerAction(ctx context.Context, container con
return d.client.ContainerActions(ctx, action, container.ID)
}
type pullEvent struct {
Status string `json:"status"`
ProgressDetail struct {
Current int64 `json:"current"`
Total int64 `json:"total"`
} `json:"progressDetail"`
ID string `json:"id"`
}
func (d *DockerClientService) UpdateContainer(ctx context.Context, c container.Container, progressCh chan<- container.UpdateProgress) 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
}
imageName := inspectResp.Config.Image
// 2. Pull image with progress
reader, err := d.client.ImagePull(ctx, imageName)
if err != nil {
progressCh <- container.UpdateProgress{Status: "error", Error: fmt.Sprintf("pull failed: %v", err)}
return err
}
defer reader.Close()
updated := false
decoder := json.NewDecoder(reader)
for {
var event pullEvent
if err := decoder.Decode(&event); err == io.EOF {
break
} else if err != nil {
progressCh <- container.UpdateProgress{Status: "error", Error: fmt.Sprintf("pull decode failed: %v", err)}
return err
}
progressCh <- container.UpdateProgress{
Status: "pulling",
Layer: event.ID,
Current: event.ProgressDetail.Current,
Total: event.ProgressDetail.Total,
}
if strings.HasPrefix(event.Status, "Status: Downloaded newer image") {
updated = true
}
}
// 3. If no new layers, report up-to-date
if !updated {
progressCh <- container.UpdateProgress{Status: "up-to-date"}
return nil
}
// 4. Check if this is a swarm service
serviceName := c.Labels["com.docker.swarm.service.name"]
if serviceName != "" {
progressCh <- container.UpdateProgress{Status: "recreating"}
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
}
progressCh <- container.UpdateProgress{Status: "done"}
return nil
}
// 5. Standalone container: stop -> remove -> create -> start
progressCh <- container.UpdateProgress{Status: "recreating"}
containerName := strings.TrimPrefix(inspectResp.Name, "/")
// Stop if running
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
}
}
// 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
}
// 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
}
// 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
}
progressCh <- container.UpdateProgress{Status: "done"}
return nil
}
func (d *DockerClientService) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
return d.store.ListContainers(labels)
}
+6
View File
@@ -2,6 +2,7 @@ package k8s_support
import (
"context"
"fmt"
"io"
"sync"
@@ -92,6 +93,11 @@ 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 {
defer close(progressCh)
return 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 {
cancelCtx, cancel := context.WithCancel(ctx)
session, err := k.client.ContainerAttach(cancelCtx, c.ID)
+50 -3
View File
@@ -5,12 +5,13 @@ import (
"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/container"
container_support "github.com/amir20/dozzle/internal/support/container"
support_web "github.com/amir20/dozzle/internal/support/web"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
)
func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
action := chi.URLParam(r, "action")
func (h *handler) findContainerWithActions(w http.ResponseWriter, r *http.Request) (*container_support.ContainerService, bool) {
id := chi.URLParam(r, "id")
userLabels := h.config.Labels
@@ -26,13 +27,24 @@ func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
if !permit {
log.Warn().Msg("user is not permitted to perform actions on container")
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
return nil, false
}
containerService, err := h.hostService.FindContainer(hostKey(r), id, userLabels)
if err != nil {
log.Error().Err(err).Msg("error while trying to find container")
http.Error(w, err.Error(), http.StatusNotFound)
return nil, false
}
return containerService, true
}
func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
action := chi.URLParam(r, "action")
containerService, ok := h.findContainerWithActions(w, r)
if !ok {
return
}
@@ -52,3 +64,38 @@ func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
log.Info().Str("action", action).Str("container", containerService.Container.Name).Msg("container action performed")
http.Error(w, "", http.StatusNoContent)
}
func (h *handler) containerUpdate(w http.ResponseWriter, r *http.Request) {
containerService, ok := h.findContainerWithActions(w, r)
if !ok {
return
}
sse, err := support_web.NewSSEWriter(r.Context(), w, r)
if err != nil {
log.Error().Err(err).Msg("error creating SSE writer")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer sse.Close()
progressCh := make(chan container.UpdateProgress, 50)
errCh := make(chan error, 1)
go func() {
errCh <- containerService.Update(r.Context(), progressCh)
}()
for progress := range progressCh {
if err := sse.Event("update-progress", progress); err != nil {
log.Error().Err(err).Msg("error writing SSE event")
return
}
}
if err := <-errCh; err != nil {
log.Error().Err(err).Msg("container update failed")
}
log.Info().Str("container", containerService.Container.Name).Msg("container update completed")
}
+76
View File
@@ -2,12 +2,15 @@ package web
import (
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/amir20/dozzle/internal/container"
docker_types "github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -90,3 +93,76 @@ func Test_handler_containerActions_start(t *testing.T) {
handler.ServeHTTP(rr, req)
assert.Equal(t, 204, rr.Code)
}
func Test_handler_containerUpdate_up_to_date(t *testing.T) {
mockedClient := mockedClient()
inspectResp := docker_types.InspectResponse{
Config: &docker_types.Config{
Image: "test:v1",
},
}
mockedClient.On("ContainerInspect", mock.Anything, "123").Return(inspectResp, nil)
pullResp := `{"status":"Already exists","id":"abc123"}` + "\n" +
`{"status":"Status: Image is up to date for test:v1"}` + "\n"
mockedClient.On("ImagePull", mock.Anything, "test:v1").Return(io.NopCloser(strings.NewReader(pullResp)), nil)
handler := createHandler(mockedClient, nil, Config{Base: "/", EnableActions: true, Authorization: Authorization{Provider: NONE}})
req, err := http.NewRequest("POST", "/api/hosts/localhost/containers/123/actions/update", nil)
require.NoError(t, err)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, 200, rr.Code)
assert.Contains(t, rr.Body.String(), `"up-to-date"`)
}
func Test_handler_containerUpdate_new_image(t *testing.T) {
m := new(MockedClient)
c := container.Container{ID: "123"}
m.On("FindContainer", mock.Anything, "123").Return(c, nil)
m.On("ContainerActions", mock.Anything, container.Start, "new-123").Return(nil)
m.On("Host").Return(container.Host{ID: "localhost"})
m.On("ListContainers", mock.Anything, mock.Anything).Return([]container.Container{c}, nil)
m.On("ContainerEvents", mock.Anything, mock.Anything).Return(nil)
inspectResp := docker_types.InspectResponse{
ContainerJSONBase: &docker_types.ContainerJSONBase{
Name: "/test-container",
},
Config: &docker_types.Config{
Image: "test:v1",
},
NetworkSettings: &docker_types.NetworkSettings{},
}
m.On("ContainerInspect", mock.Anything, "123").Return(inspectResp, nil)
pullResp := `{"status":"Already exists","id":"abc123"}` + "\n" +
`{"status":"Status: Downloaded newer image for test:v1"}` + "\n"
m.On("ImagePull", mock.Anything, "test:v1").Return(io.NopCloser(strings.NewReader(pullResp)), nil)
m.On("ContainerRemove", mock.Anything, "123").Return(nil)
m.On("ContainerCreate", mock.Anything, mock.Anything, "test-container").Return("new-123", nil)
handler := createHandler(m, nil, Config{Base: "/", EnableActions: true, Authorization: Authorization{Provider: NONE}})
req, err := http.NewRequest("POST", "/api/hosts/localhost/containers/123/actions/update", nil)
require.NoError(t, err)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, 200, rr.Code)
assert.Contains(t, rr.Body.String(), `"done"`)
}
func Test_handler_containerUpdate_not_found(t *testing.T) {
mockedClient := mockedClient()
handler := createHandler(mockedClient, nil, Config{Base: "/", EnableActions: true, Authorization: Authorization{Provider: NONE}})
req, err := http.NewRequest("POST", "/api/hosts/localhost/containers/456/actions/update", nil)
require.NoError(t, err)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, 404, rr.Code)
}
+1
View File
@@ -145,6 +145,7 @@ func createRouter(h *handler) *chi.Mux {
// Action
if h.config.EnableActions {
r.Post("/hosts/{host}/containers/{id}/actions/update", h.containerUpdate)
r.Post("/hosts/{host}/containers/{id}/actions/{action}", h.containerActions)
}
if h.config.EnableShell {
+28 -2
View File
@@ -10,6 +10,7 @@ import (
"github.com/amir20/dozzle/internal/container"
docker_support "github.com/amir20/dozzle/internal/support/docker"
docker_types "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/system"
"github.com/go-chi/chi/v5"
@@ -33,6 +34,31 @@ func (m *MockedClient) ContainerActions(ctx context.Context, action container.Co
return args.Error(0)
}
func (m *MockedClient) ImagePull(ctx context.Context, imageName string) (io.ReadCloser, error) {
args := m.Called(ctx, imageName)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
func (m *MockedClient) ContainerInspect(ctx context.Context, containerID string) (docker_types.InspectResponse, error) {
args := m.Called(ctx, containerID)
return args.Get(0).(docker_types.InspectResponse), args.Error(1)
}
func (m *MockedClient) ContainerRemove(ctx context.Context, containerID string) error {
args := m.Called(ctx, containerID)
return args.Error(0)
}
func (m *MockedClient) ContainerCreate(ctx context.Context, inspectResp docker_types.InspectResponse, name string) (string, error) {
args := m.Called(ctx, inspectResp, name)
return args.Get(0).(string), args.Error(1)
}
func (m *MockedClient) ServiceUpdate(ctx context.Context, serviceID string, imageName string) error {
args := m.Called(ctx, serviceID, imageName)
return args.Error(0)
}
func (m *MockedClient) ContainerEvents(ctx context.Context, events chan<- container.ContainerEvent) error {
args := m.Called(ctx, events)
return args.Error(0)
@@ -70,7 +96,7 @@ func (m *MockedClient) SystemInfo() system.Info {
return system.Info{ID: "123"}
}
func createHandler(client container.Client, content fs.FS, config Config) *chi.Mux {
func createHandler(client docker_support.DockerUpdateClient, content fs.FS, config Config) *chi.Mux {
if client == nil {
client = new(MockedClient)
client.(*MockedClient).On("ListContainers", mock.Anything, mock.Anything).Return([]container.Container{}, nil)
@@ -95,6 +121,6 @@ func createHandler(client container.Client, content fs.FS, config Config) *chi.M
})
}
func createDefaultHandler(client container.Client) *chi.Mux {
func createDefaultHandler(client docker_support.DockerUpdateClient) *chi.Mux {
return createHandler(client, nil, Config{Base: "/", Authorization: Authorization{Provider: NONE}})
}
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Stop
start: Start
restart: Genstart
update: Opdater
update-service: Opdater Tjeneste
update-pulling: Henter seneste image...
update-recreating: Genskaber container...
update-done: Container opdateret succesfuldt
update-up-to-date: Allerede opdateret
show-hostname: Vis værtsnavn
show-container-name: Vis containernavn
shell: Shell
@@ -59,6 +65,13 @@ error:
copy-not-supported-hint: Udklipsholder ikke tilgængelig. Kopiér linket nedenfor
logs-skipped: Vis {total} skjulte indtastninger
container-not-found: Container ikke fundet
action-failed: Handling Mislykkedes
update-failed: Opdatering Mislykkedes
unable-to-complete-action: Kan ikke fuldføre handling
invalid-action: Ugyldig handling
unable-to-update: Kan ikke opdatere container
something-went-wrong: Noget gik galt
unknown-error: Ukendt fejl
events-stream:
title: Uventet fejl
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Stop
start: Start
restart: Neustarten
update: Aktualisieren
update-service: Service Aktualisieren
update-pulling: Neuestes Image wird geladen...
update-recreating: Container wird neu erstellt...
update-done: Container erfolgreich aktualisiert
update-up-to-date: Bereits aktuell
show-hostname: Host-Name anzeigen
show-container-name: Container-Name anzeigen
shell: Shell
@@ -59,6 +65,13 @@ error:
copy-not-supported-hint: Zwischenablage nicht verfügbar. Link unten kopieren
logs-skipped: Zeige {total} versteckte Einträge
container-not-found: Container nicht gefunden.
action-failed: Aktion fehlgeschlagen
update-failed: Aktualisierung fehlgeschlagen
unable-to-complete-action: Aktion konnte nicht abgeschlossen werden
invalid-action: Ungültige Aktion
unable-to-update: Container konnte nicht aktualisiert werden
something-went-wrong: Etwas ist schiefgelaufen
unknown-error: Unbekannter Fehler
events-stream:
title: Unerwarteter Fehler
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Stop
start: Start
restart: Restart
update: Update
update-service: Update Service
update-pulling: Pulling latest image...
update-recreating: Recreating container...
update-done: Container updated successfully
update-up-to-date: Already up to date
show-hostname: Show hostname
show-container-name: Show container name
shell: Shell
@@ -61,6 +67,13 @@ error:
copy-not-supported-hint: Clipboard unavailable. Copy the link below
logs-skipped: Show {total} hidden entries
container-not-found: Container not found
action-failed: Action Failed
update-failed: Update Failed
unable-to-complete-action: Unable to complete action
invalid-action: Invalid action
unable-to-update: Unable to update container
something-went-wrong: Something went wrong
unknown-error: Unknown error
events-stream:
title: Unexpected Error
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Detener
start: Iniciar
restart: Reiniciar
update: Actualizar
update-service: Actualizar Servicio
update-pulling: Descargando última imagen...
update-recreating: Recreando contenedor...
update-done: Contenedor actualizado correctamente
update-up-to-date: Ya está actualizado
show-hostname: Mostrar nombre del host
show-container-name: Mostrar nombre del contenedor
shell: Shell
@@ -59,6 +65,13 @@ error:
copy-not-supported-hint: Portapapeles no disponible. Copie el enlace a continuación
logs-skipped: Mostrar {total} entradas ocultas
container-not-found: Contenedor no encontrado.
action-failed: Acción Fallida
update-failed: Actualización Fallida
unable-to-complete-action: No se pudo completar la acción
invalid-action: Acción no válida
unable-to-update: No se pudo actualizar el contenedor
something-went-wrong: Algo salió mal
unknown-error: Error desconocido
events-stream:
title: Error inesperado
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Stop
start: Démarrer
restart: Redémarrer
update: Mettre à jour
update-service: Mettre à jour le Service
update-pulling: Téléchargement de la dernière image...
update-recreating: Recréation du conteneur...
update-done: Conteneur mis à jour avec succès
update-up-to-date: Déjà à jour
show-hostname: Afficher le nom d'hôte
show-container-name: Afficher le nom du conteneur
shell: Shell
@@ -59,6 +65,13 @@ error:
copy-not-supported-hint: Presse-papiers indisponible. Copiez le lien ci-dessous
logs-skipped: Afficher {total} entrées cachées
container-not-found: Conteneur non trouvé
action-failed: Action Échouée
update-failed: Mise à jour Échouée
unable-to-complete-action: Impossible de terminer l'action
invalid-action: Action non valide
unable-to-update: Impossible de mettre à jour le conteneur
something-went-wrong: Quelque chose s'est mal passé
unknown-error: Erreur inconnue
events-stream:
title: Erreur inattendue
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Hentikan
start: Mulai
restart: Mulai ulang
update: Perbarui
update-service: Perbarui Layanan
update-pulling: Mengunduh image terbaru...
update-recreating: Membuat ulang kontainer...
update-done: Kontainer berhasil diperbarui
update-up-to-date: Sudah versi terbaru
show-hostname: Tampilkan nama host
show-container-name: Tampilkan nama kontainer
shell: Shell
@@ -62,6 +68,13 @@ error:
copy-not-supported-hint: Clipboard tidak tersedia. Salin tautan di bawah ini
logs-skipped: Tampilkan {total} entri tersembunyi
container-not-found: Kontainer tidak ditemukan
action-failed: Tindakan Gagal
update-failed: Pembaruan Gagal
unable-to-complete-action: Tidak dapat menyelesaikan tindakan
invalid-action: Tindakan tidak valid
unable-to-update: Tidak dapat memperbarui kontainer
something-went-wrong: Terjadi kesalahan
unknown-error: Kesalahan tidak diketahui
events-stream:
title: Kesalahan Tak Terduga
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Stop
start: Start
restart: Restart
update: Aggiorna
update-service: Aggiorna Servizio
update-pulling: Download dell'ultima immagine...
update-recreating: Ricreazione del container...
update-done: Container aggiornato con successo
update-up-to-date: Già aggiornato
show-hostname: Mostra nome host
show-container-name: Mostra nome container
shell: Shell
@@ -59,6 +65,13 @@ error:
copy-not-supported-hint: Appunti non disponibili. Copia il link qui sotto
logs-skipped: Mostra {total} voci nascoste
container-not-found: Container non trovato
action-failed: Azione Fallita
update-failed: Aggiornamento Fallito
unable-to-complete-action: Impossibile completare l'azione
invalid-action: Azione non valida
unable-to-update: Impossibile aggiornare il container
something-went-wrong: Qualcosa è andato storto
unknown-error: Errore sconosciuto
events-stream:
title: Errore inaspettato
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: 중지
start: 시작
restart: 다시 시작
update: 업데이트
update-service: 서비스 업데이트
update-pulling: 최신 이미지 가져오는 중...
update-recreating: 컨테이너 재생성 중...
update-done: 컨테이너가 성공적으로 업데이트되었습니다
update-up-to-date: 이미 최신 버전입니다
show-hostname: 호스트 이름 보기
show-container-name: 컨테이너 이름 보기
shell:
@@ -58,6 +64,13 @@ error:
copy-not-supported-hint: 클립보드를 사용할 수 없습니다. 아래 링크를 복사하세요
logs-skipped: 숨겨진 항목 {total}개 보기
container-not-found: 컨테이너를 찾을 수 없습니다
action-failed: 작업 실패
update-failed: 업데이트 실패
unable-to-complete-action: 작업을 완료할 수 없습니다
invalid-action: 잘못된 작업
unable-to-update: 컨테이너를 업데이트할 수 없습니다
something-went-wrong: 문제가 발생했습니다
unknown-error: 알 수 없는 오류
events-stream:
title: 예상치 못한 오류
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Stoppen
start: Starten
restart: Herstarten
update: Bijwerken
update-service: Service Bijwerken
update-pulling: Laatste image ophalen...
update-recreating: Container opnieuw aanmaken...
update-done: Container succesvol bijgewerkt
update-up-to-date: Al bijgewerkt
show-hostname: Toon hostnaam
show-container-name: Toon containernaam
shell: Shell
@@ -59,6 +65,13 @@ error:
copy-not-supported-hint: Klembord niet beschikbaar. Kopieer de link hieronder
logs-skipped: Toon {total} verborgen items
container-not-found: Container niet gevonden
action-failed: Actie Mislukt
update-failed: Update Mislukt
unable-to-complete-action: Kan actie niet voltooien
invalid-action: Ongeldige actie
unable-to-update: Kan container niet bijwerken
something-went-wrong: Er is iets misgegaan
unknown-error: Onbekende fout
events-stream:
title: Onverwachte fout
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Stop
start: Start
restart: Restart
update: Aktualizuj
update-service: Aktualizuj Usługę
update-pulling: Pobieranie najnowszego obrazu...
update-recreating: Tworzenie kontenera od nowa...
update-done: Kontener zaktualizowany pomyślnie
update-up-to-date: Już aktualny
show-hostname: Pokaż nazwę hosta
show-container-name: Pokaż nazwę kontenera
shell: Shell
@@ -63,6 +69,13 @@ error:
copy-not-supported-hint: Schowek niedostępny. Skopiuj poniższy link
logs-skipped: Pokaż {total} ukrytych wpisów
container-not-found: Kontener nie został znaleziony
action-failed: Akcja Nieudana
update-failed: Aktualizacja Nieudana
unable-to-complete-action: Nie można ukończyć akcji
invalid-action: Nieprawidłowa akcja
unable-to-update: Nie można zaktualizować kontenera
something-went-wrong: Coś poszło nie tak
unknown-error: Nieznany błąd
events-stream:
title: Niespodziewany błąd
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Parar
start: Iniciar
restart: Reiniciar
update: Swab the Decks
update-service: Update th' Service
update-pulling: Haulin' the latest image...
update-recreating: Rebuildin' the vessel...
update-done: Container updated, arr!
update-up-to-date: Already shipshape, cap'n!
show-hostname: Mostrar nome do anfitrião
show-container-name: Mostrar nome do contentor
shell: Shell
@@ -62,6 +68,13 @@ error:
copy-not-supported-hint: Área de transferência indisponível. Copie o link abaixo
logs-skipped: Mostrar {total} entradas ocultas
container-not-found: Contentor não encontrado.
action-failed: Action Be Cursed!
update-failed: Update Be Scuttled!
unable-to-complete-action: Can't finish the deed, matey
invalid-action: That be no proper action
unable-to-update: Can't swab this container
something-went-wrong: Blimey, somethin' went wrong!
unknown-error: Unknown curse upon us
events-stream:
title: Erro inesperado
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Parar
start: Iniciar
restart: Reiniciar
update: Atualizar
update-service: Atualizar Serviço
update-pulling: Baixando imagem mais recente...
update-recreating: Recriando container...
update-done: Container atualizado com sucesso
update-up-to-date: Já está atualizado
show-hostname: Mostrar nome do host
show-container-name: Mostrar nome do container
shell: Shell
@@ -58,6 +64,13 @@ error:
copy-not-supported-hint: Área de transferência indisponível. Copie o link abaixo
logs-skipped: Mostrar {total} entradas ocultas
container-not-found: Container não encontrado
action-failed: Ação Falhou
update-failed: Atualização Falhou
unable-to-complete-action: Não foi possível completar a ação
invalid-action: Ação inválida
unable-to-update: Não foi possível atualizar o container
something-went-wrong: Algo deu errado
unknown-error: Erro desconhecido
events-stream:
title: Erro Inesperado
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Остановить
start: Запустить
restart: Перезапустить
update: Обновить
update-service: Обновить Сервис
update-pulling: Загрузка последнего образа...
update-recreating: Пересоздание контейнера...
update-done: Контейнер успешно обновлён
update-up-to-date: Уже обновлён
show-hostname: Показать имя хоста
show-container-name: Показать имя контейнера
shell: Оболочка
@@ -59,6 +65,13 @@ error:
copy-not-supported-hint: Буфер обмена недоступен. Скопируйте ссылку ниже
logs-skipped: Показать {total} скрытых записей
container-not-found: Контейнер не найден.
action-failed: Действие не выполнено
update-failed: Обновление не выполнено
unable-to-complete-action: Не удалось выполнить действие
invalid-action: Недопустимое действие
unable-to-update: Не удалось обновить контейнер
something-went-wrong: Что-то пошло не так
unknown-error: Неизвестная ошибка
events-stream:
title: Неожиданная ошибка
message: >-
+13
View File
@@ -15,6 +15,12 @@ toolbar:
copy-filtered-logs: Kopiraj filtrirane dnevnike
copying-logs: Kopiranje dnevnikov...
restart: Ponovni zagon
update: Posodobi
update-service: Posodobi Storitev
update-pulling: Prenašanje najnovejše slike...
update-recreating: Ponovno ustvarjanje zabojnika...
update-done: Zabojnik uspešno posodobljen
update-up-to-date: Že posodobljen
show-hostname: Prikaži ime gostitelja
action:
copy-log: Kopiraj dnevnik
@@ -67,6 +73,13 @@ error:
message: Časovna omejitev uporabniškega vmesnika Dozzle je potekla med
povezovanjem z API-jem. Preverite omrežno povezavo in poskusite znova.
container-not-found: Zabojnika ni bilo mogoče najti
action-failed: Dejanje Neuspešno
update-failed: Posodobitev Neuspešna
unable-to-complete-action: Dejanja ni mogoče dokončati
invalid-action: Neveljavno dejanje
unable-to-update: Zabojnika ni mogoče posodobiti
something-went-wrong: Nekaj je šlo narobe
unknown-error: Neznana napaka
alert:
redirected:
title: Preusmerjeno v nov zabojnik
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: Durdur
start: Başlat
restart: Yeniden Başlat
update: Güncelle
update-service: Servisi Güncelle
update-pulling: En son imaj indiriliyor...
update-recreating: Konteyner yeniden oluşturuluyor...
update-done: Konteyner başarıyla güncellendi
update-up-to-date: Zaten güncel
show-hostname: Sunucu adını göster
show-container-name: Konteyner adını göster
shell: Kabuk
@@ -64,6 +70,13 @@ error:
copy-not-supported-hint: Pano kullanılamıyor. Aşağıdaki bağlantıyı kopyalayın
logs-skipped: "{total} gizli girişi göster"
container-not-found: Konteyner bulunamadı
action-failed: İşlem Başarısız
update-failed: Güncelleme Başarısız
unable-to-complete-action: İşlem tamamlanamadı
invalid-action: Geçersiz işlem
unable-to-update: Konteyner güncellenemedi
something-went-wrong: Bir şeyler ters gitti
unknown-error: Bilinmeyen hata
events-stream:
title: Beklenmeyen Hata
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: 停止
start: 啟動
restart: 重新啟動
update: 更新
update-service: 更新服務
update-pulling: 正在拉取最新映像檔...
update-recreating: 正在重新建立容器...
update-done: 容器更新成功
update-up-to-date: 已是最新版本
show-hostname: 顯示主機名稱
show-container-name: 顯示容器名稱
shell: 終端機
@@ -59,6 +65,13 @@ error:
copy-not-supported-hint: 剪貼簿不可用。請複製下方連結
logs-skipped: 顯示 {total} 個隱藏項目
container-not-found: 找不到容器
action-failed: 操作失敗
update-failed: 更新失敗
unable-to-complete-action: 無法完成操作
invalid-action: 無效的操作
unable-to-update: 無法更新容器
something-went-wrong: 發生錯誤
unknown-error: 未知錯誤
events-stream:
title: 意外錯誤
message: >-
+13
View File
@@ -8,6 +8,12 @@ toolbar:
stop: 停止
start: 启动
restart: 重启
update: 更新
update-service: 更新服务
update-pulling: 正在拉取最新镜像...
update-recreating: 正在重新创建容器...
update-done: 容器更新成功
update-up-to-date: 已是最新版本
show-hostname: 显示主机名
show-container-name: 显示容器名称
shell: 终端
@@ -59,6 +65,13 @@ error:
copy-not-supported-hint: 剪贴板不可用。请复制下方链接
logs-skipped: 显示 {total} 个隐藏条目
container-not-found: 容器未找到。
action-failed: 操作失败
update-failed: 更新失败
unable-to-complete-action: 无法完成操作
invalid-action: 无效操作
unable-to-update: 无法更新容器
something-went-wrong: 出了点问题
unknown-error: 未知错误
events-stream:
title: 意外错误
message: >-
+13
View File
@@ -18,6 +18,7 @@ service AgentService {
rpc StreamContainerStarted(StreamContainerStartedRequest) returns (stream StreamContainerStartedResponse) {}
rpc HostInfo(HostInfoRequest) returns (HostInfoResponse) {}
rpc ContainerAction(ContainerActionRequest) returns (ContainerActionResponse) {}
rpc UpdateContainer(UpdateContainerRequest) returns (stream UpdateContainerProgress) {}
rpc ContainerExec(stream ContainerExecRequest) returns (stream ContainerExecResponse) {}
rpc ContainerAttach(stream ContainerAttachRequest) returns (stream ContainerAttachResponse) {}
rpc UpdateNotificationConfig(UpdateNotificationConfigRequest) returns (UpdateNotificationConfigResponse) {}
@@ -100,6 +101,18 @@ message ContainerActionRequest {
message ContainerActionResponse {}
message UpdateContainerRequest {
string containerId = 1;
}
message UpdateContainerProgress {
string status = 1;
string layer = 2;
int64 current = 3;
int64 total = 4;
string error = 5;
}
message ContainerExecRequest {
string containerId = 1;
repeated string command = 2;