Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/web/projects/vgpu/views/task/admin/Detail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@
<span class="summary-item-label">{{ $t('task.createTime') }}</span>
<span class="summary-item-value">{{ basicCreateTime }}</span>
</div>
<div v-if="detail.labels && Object.keys(detail.labels).length" class="summary-item">
<span class="summary-item-label">{{ $t('task.labels') }}</span>
<span class="summary-item-value">
<t-tag
v-for="(v, k) in detail.labels"
:key="k"
theme="default"
variant="light"
size="small"
style="margin: 2px 4px 2px 0"
>{{ k }}={{ v }}</t-tag>
</span>
</div>
</div>
</div>
</div>
Expand Down
23 changes: 23 additions & 0 deletions packages/web/projects/vgpu/views/task/admin/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,29 @@ const baseColumns = computed(() => [
);
},
},
{
title: t('task.labels'),
dataIndex: 'labels',
render: ({ labels }) => {
if (!labels || typeof labels !== 'object') return <span>--</span>;
const entries = Object.entries(labels);
if (entries.length === 0) return <span>--</span>;
const visible = entries.slice(0, 2);
const remaining = entries.length - 2;
return (
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap', maxWidth: '220px' }}>
{visible.map(([k, v]) => (
<t-tag key={k} theme="default" variant="light" size="small" style={{ maxWidth: '100px' }}>
{k}={v}
</t-tag>
))}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{remaining > 0 && (
<t-tag theme="warning" variant="light" size="small">+{remaining}</t-tag>
)}
</div>
);
},
},
{
title: t('task.status'),
dataIndex: 'status',
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export default {
priority: 'Priority',
appName: 'App Name',
createTime: 'Creation Time',
labels: 'Labels',
times: 'x',
computeUsageTrend: 'GPU Compute Utilization (%)',
memUsageTrend: 'GPU Memory Utilization (%)',
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ export default {
priority: '优先级',
appName: '应用名称',
createTime: '创建时间',
labels: '标签',
times: '倍',
computeUsageTrend: 'GPU 算力使用率(%)',
memUsageTrend: 'GPU 显存使用率(%)',
Expand Down
1 change: 1 addition & 0 deletions server/api/v1/container.proto
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ message ContainerReply {
string namespace = 17;
repeated string device_ids = 18;
repeated string images = 19;
map<string, string> labels = 20;
}

message ContainersReply {
Expand Down
1 change: 1 addition & 0 deletions server/internal/biz/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Container struct {
Priority string
NodeUID string
Namespace string
Labels map[string]string
}

type PodInfo struct {
Expand Down
5 changes: 5 additions & 0 deletions server/internal/data/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ func (r *podRepo) fetchContainerInfo(pod *corev1.Pod) []*biz.Container {
if deviceIdx < len(bizContainerDevices) {
containerDevices = bizContainerDevices[deviceIdx]
}
labels := make(map[string]string, len(pod.Labels))
for k, v := range pod.Labels {
labels[k] = v
}
c := &biz.Container{
Name: ctr.Name,
UUID: ctrIdMaps[ctr.Name],
Expand All @@ -152,6 +156,7 @@ func (r *podRepo) fetchContainerInfo(pod *corev1.Pod) []*biz.Container {
Namespace: pod.Namespace,
CreateTime: r.GetCreateTime(pod),
ContainerDevices: containerDevices,
Labels: labels,
}
if len(containerDevices) > 0 {
c.Priority = containerDevices[0].Priority
Expand Down
2 changes: 2 additions & 0 deletions server/internal/service/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func (s *ContainerService) GetAllContainers(ctx context.Context, req *pb.GetAllC
containerReply.NodeUid = container.NodeUID
containerReply.Namespace = container.Namespace
containerReply.Priority = container.Priority
containerReply.Labels = container.Labels
for _, containerDevice := range container.ContainerDevices {
deviceID := containerDevice.UUID
if device, err := s.node.FindDeviceByAliasId(containerDevice.UUID); err == nil {
Expand Down Expand Up @@ -127,6 +128,7 @@ func (s *ContainerService) GetContainer(ctx context.Context, req *pb.GetContaine
ctrReply.NodeUid = container.NodeUID
ctrReply.Namespace = container.Namespace
ctrReply.Priority = container.Priority
ctrReply.Labels = container.Labels
allContainers, err := s.pod.ListAllContainers(ctx)
if err == nil {
images := make([]string, 0)
Expand Down
221 changes: 221 additions & 0 deletions server/internal/service/container_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package service

import (
"context"
"io"
"testing"
"time"

"github.com/go-kratos/kratos/v2/log"
pb "vgpu/api/v1"
"vgpu/internal/biz"
)

// mockPodRepo implements biz.PodRepo.
type mockPodRepo struct {
containers []*biz.Container
err error
}

func (m *mockPodRepo) ListAll(_ context.Context) ([]*biz.Container, error) {
return m.containers, m.err
}

func (m *mockPodRepo) FindOne(_ context.Context, podUID string, name string) (*biz.Container, error) {
for _, c := range m.containers {
if c.PodUID == podUID && c.Name == name {
return c, nil
}
}
return nil, m.err
}

// mockNodeRepo implements biz.NodeRepo.
type mockNodeRepo struct{}

func (m *mockNodeRepo) ListAll(_ context.Context) ([]*biz.Node, error) {
return nil, nil
}

func (m *mockNodeRepo) GetNode(_ context.Context, _ string) (*biz.Node, error) {
return nil, nil
}

func (m *mockNodeRepo) ListAllDevices(_ context.Context) ([]*biz.DeviceInfo, error) {
return nil, nil
}

func (m *mockNodeRepo) FindDeviceByAliasId(_ string) (*biz.DeviceInfo, error) {
return nil, io.ErrUnexpectedEOF // return error so the service falls through to raw UUID
}

func TestContainerService_GetAllContainers_Labels(t *testing.T) {
logger := log.NewStdLogger(io.Discard)

mockPod := &mockPodRepo{
containers: []*biz.Container{
{
Name: "ctr-1",
PodUID: "pod-1",
PodName: "my-app",
Status: biz.ContainerStatusSuccess,
Namespace: "default",
NodeName: "node-1",
NodeUID: "node-uid-1",
Priority: "1",
Image: "nginx:latest",
ContainerDevices: biz.ContainerDevices{
{UUID: "gpu-uuid-1", Type: "NVIDIA", Usedmem: 1000, Usedcores: 10},
},
Labels: map[string]string{
"app": "test-app",
"version": "v1",
},
},
},
}

mockNode := &mockNodeRepo{}

svc := NewContainerService(biz.NewNodeUsecase(mockNode, logger), biz.NewPodUseCase(mockPod, logger))

res, err := svc.GetAllContainers(context.Background(), &pb.GetAllContainersReq{
Filters: &pb.GetAllContainersReq_Filters{},
})
if err != nil {
t.Fatalf("GetAllContainers failed: %v", err)
}
if len(res.Items) != 1 {
t.Fatalf("expected 1 item, got %d", len(res.Items))
}

ctr := res.Items[0]
labels := ctr.Labels
if labels == nil {
t.Fatal("expected Labels to be non-nil")
}
if len(labels) != 2 {
t.Fatalf("expected 2 labels, got %d", len(labels))
}
if v, ok := labels["app"]; !ok || v != "test-app" {
t.Fatalf(`expected labels["app"] == "test-app", got %q`, v)
}
if v, ok := labels["version"]; !ok || v != "v1" {
t.Fatalf(`expected labels["version"] == "v1", got %q`, v)
}

// Verify the rest of the mapping still works
if len(ctr.DeviceIds) != 1 || ctr.DeviceIds[0] != "gpu-uuid-1" {
t.Fatalf("expected DeviceIds [gpu-uuid-1], got %v", ctr.DeviceIds)
}
}

func TestContainerService_GetContainer_Labels(t *testing.T) {
logger := log.NewStdLogger(io.Discard)

mockPod := &mockPodRepo{
containers: []*biz.Container{
{
Name: "ctr-1",
PodUID: "pod-1",
PodName: "my-app",
Status: biz.ContainerStatusSuccess,
Namespace: "default",
NodeName: "node-1",
NodeUID: "node-uid-1",
Priority: "1",
Image: "nginx:latest",
CreateTime: mustParseTime("2025-01-01T00:00:00Z"),
ContainerDevices: biz.ContainerDevices{
{UUID: "gpu-uuid-1", Type: "NVIDIA", Usedmem: 1000, Usedcores: 10},
},
Labels: map[string]string{
"app": "test-app",
"version": "v1",
},
},
},
}

mockNode := &mockNodeRepo{}

svc := NewContainerService(biz.NewNodeUsecase(mockNode, logger), biz.NewPodUseCase(mockPod, logger))

ctrReply, err := svc.GetContainer(context.Background(), &pb.GetContainerReq{
PodUid: "pod-1",
Name: "ctr-1",
})
if err != nil {
t.Fatalf("GetContainer failed: %v", err)
}

labels := ctrReply.Labels
if labels == nil {
t.Fatal("expected Labels to be non-nil")
}
if len(labels) != 2 {
t.Fatalf("expected 2 labels, got %d", len(labels))
}
if v, ok := labels["app"]; !ok || v != "test-app" {
t.Fatalf(`expected labels["app"] == "test-app", got %q`, v)
}
if v, ok := labels["version"]; !ok || v != "v1" {
t.Fatalf(`expected labels["version"] == "v1", got %q`, v)
}

if len(ctrReply.DeviceIds) != 1 || ctrReply.DeviceIds[0] != "gpu-uuid-1" {
t.Fatalf("expected DeviceIds [gpu-uuid-1], got %v", ctrReply.DeviceIds)
}
}

func TestContainerService_GetAllContainers_NilLabels(t *testing.T) {
logger := log.NewStdLogger(io.Discard)

mockPod := &mockPodRepo{
containers: []*biz.Container{
{
Name: "ctr-nil-labels",
PodUID: "pod-2",
PodName: "no-labels-app",
Status: biz.ContainerStatusSuccess,
Namespace: "default",
NodeName: "node-1",
NodeUID: "node-uid-1",
Priority: "0",
Image: "alpine:latest",
ContainerDevices: biz.ContainerDevices{
{UUID: "gpu-uuid-2", Type: "NVIDIA", Usedmem: 500, Usedcores: 5},
},
Labels: nil,
},
},
}

mockNode := &mockNodeRepo{}

svc := NewContainerService(biz.NewNodeUsecase(mockNode, logger), biz.NewPodUseCase(mockPod, logger))

res, err := svc.GetAllContainers(context.Background(), &pb.GetAllContainersReq{
Filters: &pb.GetAllContainersReq_Filters{},
})
if err != nil {
t.Fatalf("GetAllContainers failed: %v", err)
}
if len(res.Items) != 1 {
t.Fatalf("expected 1 item, got %d", len(res.Items))
}

ctr := res.Items[0]
if ctr.Labels != nil {
t.Fatal("expected Labels to be nil for a container with nil labels")
}
}

// mustParseTime is a test helper for parsing RFC3339 timestamps.
func mustParseTime(s string) time.Time {
t, err := time.Parse(time.RFC3339, s)
if err != nil {
panic(err)
}
return t
}