Skip to content

Commit 46fb09e

Browse files
authored
feat: handles container exits better and uses the real time when container has exited (#3504)
1 parent cc620ad commit 46fb09e

File tree

16 files changed

+191
-90
lines changed

16 files changed

+191
-90
lines changed

assets/components/ContainerPopup.vue

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
<template>
22
<div>
3-
<span class="font-light capitalize"> RUNNING </span>
3+
<span class="font-light capitalize"> STATE </span>
4+
<span class="font-semibold uppercase"> {{ container.state }} </span>
5+
</div>
6+
<div v-if="container.startedAt.getFullYear() > 0">
7+
<span class="font-light capitalize"> STARTED </span>
48
<span class="font-semibold">
5-
<DistanceTime :date="container.created" strict :suffix="false" />
9+
<DistanceTime :date="container.startedAt" strict />
610
</span>
711
</div>
8-
<div>
12+
<div v-if="container.state != 'running' && container.finishedAt.getFullYear() > 0">
13+
<span class="font-light capitalize"> Finished </span>
14+
<span class="font-semibold">
15+
<DistanceTime :date="container.finishedAt" strict />
16+
</span>
17+
</div>
18+
<div v-if="container.state == 'running'">
919
<span class="font-light capitalize"> Load </span>
1020
<span class="font-semibold"> {{ container.stat.cpu.toFixed(2) }}% </span>
1121
</div>
12-
<div>
22+
<div v-if="container.state == 'running'">
1323
<span class="font-light capitalize"> MEM </span>
1424
<span class="font-semibold"> {{ formatBytes(container.stat.memoryUsage) }} </span>
1525
</div>

assets/components/FuzzySearchModal.spec.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,45 @@ function createFuzzySearchModal() {
3131
initialState: {
3232
container: {
3333
containers: [
34-
new Container("123", new Date(), "image", "test", "command", "host", {}, "running", []),
35-
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "running", []),
36-
new Container("567", new Date(), "image", "baz", "command", "host", {}, "running", []),
34+
new Container(
35+
"123",
36+
new Date(),
37+
new Date(),
38+
new Date(),
39+
"image",
40+
"test",
41+
"command",
42+
"host",
43+
{},
44+
"running",
45+
[],
46+
),
47+
new Container(
48+
"345",
49+
new Date(),
50+
new Date(),
51+
new Date(),
52+
"image",
53+
"foo bar",
54+
"command",
55+
"host",
56+
{},
57+
"running",
58+
[],
59+
),
60+
new Container(
61+
"567",
62+
new Date(),
63+
new Date(),
64+
new Date(),
65+
"image",
66+
"baz",
67+
"command",
68+
"host",
69+
{},
70+
"running",
71+
[],
72+
),
3773
],
3874
},
3975
},

assets/components/HostMenu.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
</span>
9797
</router-link>
9898
<template #content>
99-
<ContainerPopup :container="item as Container" />
99+
<ContainerPopup :container="item" />
100100
</template>
101101
</Popup>
102102
</li>

assets/components/LogViewer/EventSource.spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,19 @@ describe("<ContainerEventSource />", () => {
9494
},
9595
props: {
9696
streamSource: useContainerStream,
97-
entity: new Container("abc", new Date(), "image", "name", "command", "localhost", {}, "created", []),
97+
entity: new Container(
98+
"abc",
99+
new Date(), // created
100+
new Date(), // started
101+
new Date(), // finished
102+
"image",
103+
"name",
104+
"command",
105+
"localhost",
106+
{},
107+
"created",
108+
[],
109+
),
98110
},
99111
});
100112
}

assets/composable/eventStreams.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,12 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
137137
const event = JSON.parse((e as MessageEvent).data) as {
138138
actorId: string;
139139
name: "container-stopped" | "container-started";
140+
time: string;
140141
};
141142
const containerEvent = new ContainerEventLogEntry(
142143
event.name == "container-started" ? "Container started" : "Container stopped",
143144
event.actorId,
144-
new Date(),
145+
new Date(event.time),
145146
event.name,
146147
);
147148

assets/models/Container.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export class Container {
3030
constructor(
3131
public readonly id: string,
3232
public readonly created: Date,
33+
public startedAt: Date,
34+
public finishedAt: Date,
3335
public readonly image: string,
3436
name: string,
3537
public readonly command: string,

assets/stores/container.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,13 @@ export const useContainerStore = defineStore("container", () => {
6161
}
6262
});
6363
es.addEventListener("container-event", (e) => {
64-
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; name: string };
64+
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; name: string; time: string };
6565
const container = allContainersById.value[event.actorId];
6666
if (container) {
6767
switch (event.name) {
6868
case "die":
6969
container.state = "exited";
70+
container.finishedAt = new Date(event.time);
7071
break;
7172
case "destroy":
7273
container.state = "deleted";
@@ -75,6 +76,18 @@ export const useContainerStore = defineStore("container", () => {
7576
}
7677
});
7778

79+
es.addEventListener("container-updated", (e) => {
80+
const container = JSON.parse((e as MessageEvent).data) as ContainerJson;
81+
const existing = allContainersById.value[container.id];
82+
if (existing) {
83+
existing.name = container.name;
84+
existing.state = container.state;
85+
existing.health = container.health;
86+
existing.startedAt = new Date(container.startedAt);
87+
existing.finishedAt = new Date(container.finishedAt);
88+
}
89+
});
90+
7891
es.addEventListener("update-host", (e) => {
7992
const host = JSON.parse((e as MessageEvent).data) as Host;
8093
updateHost(host);
@@ -133,6 +146,8 @@ export const useContainerStore = defineStore("container", () => {
133146
return new Container(
134147
c.id,
135148
new Date(c.created),
149+
new Date(c.startedAt),
150+
new Date(c.finishedAt),
136151
c.image,
137152
c.name,
138153
c.command,

assets/types/Container.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ export interface ContainerStat {
77

88
export type ContainerJson = {
99
readonly id: string;
10-
readonly created: number;
10+
readonly created: string;
11+
readonly startedAt: string;
12+
readonly finishedAt: string;
1113
readonly image: string;
1214
readonly name: string;
1315
readonly command: string;

internal/docker/client.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,12 +385,16 @@ func newContainerFromJSON(c types.ContainerJSON, host string) Container {
385385
Tty: c.Config.Tty,
386386
}
387387

388+
if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
389+
container.Created = createdAt.UTC()
390+
}
391+
388392
if startedAt, err := time.Parse(time.RFC3339Nano, c.State.StartedAt); err == nil {
389393
container.StartedAt = startedAt.UTC()
390394
}
391395

392-
if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
393-
container.Created = createdAt.UTC()
396+
if stoppedAt, err := time.Parse(time.RFC3339Nano, c.State.FinishedAt); err == nil {
397+
container.FinishedAt = stoppedAt.UTC()
394398
}
395399

396400
if c.State.Health != nil {

internal/docker/container_store.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,34 @@ func (s *ContainerStore) FindContainer(id string, filter ContainerFilter) (Conta
154154
}
155155

156156
if container, ok := s.containers.Load(id); ok {
157+
if container.StartedAt.IsZero() {
158+
log.Debug().Str("id", id).Msg("container doesn't have detailed information, fetching it")
159+
if newContainer, ok := s.containers.Compute(id, func(c *Container, loaded bool) (*Container, bool) {
160+
if loaded {
161+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
162+
defer cancel()
163+
if newContainer, err := s.client.FindContainer(ctx, id); err == nil {
164+
return &newContainer, false
165+
}
166+
}
167+
return c, false
168+
}); ok {
169+
event := ContainerEvent{
170+
Name: "update",
171+
Host: s.client.Host().ID,
172+
ActorID: id,
173+
}
174+
s.subscribers.Range(func(c context.Context, events chan<- ContainerEvent) bool {
175+
select {
176+
case events <- event:
177+
case <-c.Done():
178+
s.subscribers.Delete(c)
179+
}
180+
return true
181+
})
182+
return *newContainer, nil
183+
}
184+
}
157185
return *container, nil
158186
} else {
159187
log.Warn().Str("id", id).Msg("container not found")

0 commit comments

Comments
 (0)