mirror of
https://github.com/amir20/dozzle.git
synced 2026-06-23 04:10:12 +00:00
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:
@@ -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
|
||||
|
||||
Vendored
+1
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}})
|
||||
}
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user