mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-12 16:00:33 +00:00
fix: fix container list
This commit is contained in:
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := containerService.Page(req)
|
||||
total, list, err := containerService.Page(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -249,9 +249,10 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
|
||||
// @Success 200 {object} dto.ContainerStatus
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Param containersOnly query boolean false "Only count containers"
|
||||
// @Router /containers/status [get]
|
||||
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
|
||||
data, err := containerService.LoadStatus()
|
||||
data, err := containerService.LoadStatus(c.Request.Context(), c.Query("containersOnly") == "true")
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -415,9 +416,14 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
|
||||
// @Success 200 {array} dto.ContainerListStats
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
|
||||
// @Router /containers/list/stats [get]
|
||||
func (b *BaseApi) ContainerListStats(c *gin.Context) {
|
||||
data, err := containerService.ContainerListStats()
|
||||
var ids []string
|
||||
if _, supplied := c.Request.URL.Query()["ids"]; supplied {
|
||||
ids = strings.FieldsFunc(c.Query("ids"), func(r rune) bool { return r == ',' })
|
||||
}
|
||||
data, err := containerService.ContainerListStats(c.Request.Context(), ids)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -964,3 +970,12 @@ func (b *BaseApi) ContainerStreamLogs(c *gin.Context) {
|
||||
|
||||
containerService.StreamLogs(c, streamLog)
|
||||
}
|
||||
|
||||
func (b *BaseApi) CleanNetworks(c *gin.Context) {
|
||||
result, err := containerService.CleanNetworks()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
@@ -58,10 +58,10 @@ type ContainerService struct{}
|
||||
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
|
||||
|
||||
type IContainerService interface {
|
||||
Page(req dto.PageContainer) (int64, interface{}, error)
|
||||
Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error)
|
||||
List() []dto.ContainerOptions
|
||||
ListByImage(imageName string) []dto.ContainerOptions
|
||||
LoadStatus() (dto.ContainerStatus, error)
|
||||
LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error)
|
||||
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
ListNetwork() ([]dto.Options, error)
|
||||
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
@@ -80,7 +80,7 @@ type IContainerService interface {
|
||||
ContainerUpdate(req dto.ContainerOperate) error
|
||||
ContainerUpgrade(req dto.ContainerUpgrade) error
|
||||
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
|
||||
ContainerListStats() ([]dto.ContainerListStats, error)
|
||||
ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error)
|
||||
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
|
||||
LoadResourceLimit() (*dto.ResourceLimit, error)
|
||||
ContainerRename(req dto.ContainerRename) error
|
||||
@@ -112,7 +112,9 @@ func NewIContainerService() IContainerService {
|
||||
return &ContainerService{}
|
||||
}
|
||||
|
||||
func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
|
||||
func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
@@ -123,7 +125,7 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
|
||||
options.Filters = filters.NewArgs()
|
||||
options.Filters.Add("label", req.Filters)
|
||||
}
|
||||
containers, err := client.ContainerList(context.Background(), options)
|
||||
containers, err := client.ContainerList(ctx, options)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
@@ -206,27 +208,32 @@ func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions
|
||||
return options
|
||||
}
|
||||
|
||||
func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
|
||||
func (u *ContainerService) LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error) {
|
||||
var data dto.ContainerStatus
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer client.Close()
|
||||
c := context.Background()
|
||||
|
||||
images, _ := client.ImageList(c, image.ListOptions{All: true})
|
||||
data.ImageCount = len(images)
|
||||
repo, _ := imageRepoRepo.List()
|
||||
data.RepoCount = len(repo)
|
||||
templates, _ := composeRepo.List()
|
||||
data.ComposeTemplateCount = len(templates)
|
||||
networks, _ := client.NetworkList(c, network.ListOptions{})
|
||||
data.NetworkCount = len(networks)
|
||||
volumes, _ := client.VolumeList(c, volume.ListOptions{})
|
||||
data.VolumeCount = len(volumes.Volumes)
|
||||
data.ComposeCount = loadComposeCount(client)
|
||||
containers, _ := client.ContainerList(c, container.ListOptions{All: true})
|
||||
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
if !containersOnly {
|
||||
images, _ := client.ImageList(c, image.ListOptions{All: true})
|
||||
data.ImageCount = len(images)
|
||||
repo, _ := imageRepoRepo.List()
|
||||
data.RepoCount = len(repo)
|
||||
templates, _ := composeRepo.List()
|
||||
data.ComposeTemplateCount = len(templates)
|
||||
networks, _ := client.NetworkList(c, network.ListOptions{})
|
||||
data.NetworkCount = len(networks)
|
||||
volumes, _ := client.VolumeList(c, volume.ListOptions{})
|
||||
data.VolumeCount = len(volumes.Volumes)
|
||||
data.ComposeCount = loadComposeCount(c, client)
|
||||
}
|
||||
containers, err := client.ContainerList(c, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
data.ContainerCount = len(containers)
|
||||
for _, item := range containers {
|
||||
switch item.State {
|
||||
@@ -292,27 +299,67 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) {
|
||||
func (u *ContainerService) ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
list, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
options := container.ListOptions{All: true}
|
||||
if ids != nil {
|
||||
if len(ids) == 0 {
|
||||
return []dto.ContainerListStats{}, nil
|
||||
}
|
||||
options.Filters = filters.NewArgs()
|
||||
for _, id := range ids {
|
||||
options.Filters.Add("id", id)
|
||||
}
|
||||
}
|
||||
list, err := client.ContainerList(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return collectContainerStats(ctx, list, func(ctx context.Context, id string) dto.ContainerListStats {
|
||||
return loadCpuAndMem(ctx, client, id)
|
||||
}), nil
|
||||
}
|
||||
|
||||
// A fixed worker pool bounds Docker stats requests, including for legacy callers
|
||||
// that request all containers. Stopped containers do not need a stats sample.
|
||||
func collectContainerStats(ctx context.Context, list []container.Summary, load func(context.Context, string) dto.ContainerListStats) []dto.ContainerListStats {
|
||||
datas := make([]dto.ContainerListStats, len(list))
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(list))
|
||||
for i := 0; i < len(list); i++ {
|
||||
go func(index int, item container.Summary) {
|
||||
datas[index] = loadCpuAndMem(client, item.ID)
|
||||
wg.Done()
|
||||
}(i, list[i])
|
||||
for i, item := range list {
|
||||
datas[i].ContainerID = item.ID
|
||||
}
|
||||
jobs := make(chan int)
|
||||
var wg sync.WaitGroup
|
||||
for worker := 0; worker < min(8, len(list)); worker++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for index := range jobs {
|
||||
if ctx.Err() != nil || list[index].State != "running" {
|
||||
continue
|
||||
}
|
||||
sampleCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
datas[index] = load(sampleCtx, list[index].ID)
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
dispatch:
|
||||
for index := range list {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break dispatch
|
||||
case jobs <- index:
|
||||
}
|
||||
}
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
return datas, nil
|
||||
return datas
|
||||
}
|
||||
|
||||
func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
||||
@@ -1715,11 +1762,11 @@ func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo
|
||||
return selected
|
||||
}
|
||||
|
||||
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
|
||||
func loadCpuAndMem(ctx context.Context, client *client.Client, containerItem string) dto.ContainerListStats {
|
||||
data := dto.ContainerListStats{
|
||||
ContainerID: containerItem,
|
||||
}
|
||||
res, err := client.ContainerStats(context.Background(), containerItem, false)
|
||||
res, err := client.ContainerStats(ctx, containerItem, false)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
@@ -1942,11 +1989,11 @@ func transPortToStr(ports []container.Port) []string {
|
||||
return docker.SimplifyPorts(ports)
|
||||
}
|
||||
|
||||
func loadComposeCount(client *client.Client) int {
|
||||
func loadComposeCount(ctx context.Context, client *client.Client) int {
|
||||
options := container.ListOptions{All: true}
|
||||
options.Filters = filters.NewArgs()
|
||||
options.Filters.Add("label", composeProjectLabel)
|
||||
list, err := client.ContainerList(context.Background(), options)
|
||||
list, err := client.ContainerList(ctx, options)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@ import { ResPage, SearchWithPage } from '../interface';
|
||||
import { Container } from '../interface/container';
|
||||
import { TimeoutEnum } from '@/enums/http-enum';
|
||||
|
||||
export const searchContainer = (params: Container.ContainerSearch, currentNode?: string) => {
|
||||
return http.post<ResPage<Container.ContainerInfo>>(
|
||||
`/containers/search`,
|
||||
params,
|
||||
TimeoutEnum.T_40S,
|
||||
currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
);
|
||||
export const searchContainer = (params: Container.ContainerSearch, currentNode?: string, signal?: AbortSignal) => {
|
||||
return http.postWithConfig<ResPage<Container.ContainerInfo>>(`/containers/search`, params, {
|
||||
timeout: TimeoutEnum.T_40S,
|
||||
headers: currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
export const listContainer = () => {
|
||||
return http.post<Array<Container.ContainerOption>>(`/containers/list`, {});
|
||||
@@ -44,12 +43,11 @@ export const downloadContainerFile = (params: { containerID: string; path: strin
|
||||
timeout: TimeoutEnum.T_40S,
|
||||
});
|
||||
};
|
||||
export const loadContainerStatus = (currentNode?: string) => {
|
||||
return http.get<Container.ContainerStatus>(
|
||||
`/containers/status`,
|
||||
{},
|
||||
currentNode ? { headers: { CurrentNode: currentNode } } : {},
|
||||
);
|
||||
export const loadContainerStatus = (currentNode?: string, containersOnly = false, signal?: AbortSignal) => {
|
||||
return http.get<Container.ContainerStatus>(`/containers/status`, containersOnly ? { containersOnly: true } : {}, {
|
||||
headers: currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
export const loadResourceLimit = () => {
|
||||
return http.get<Container.ResourceLimit>(`/containers/limit`);
|
||||
@@ -90,12 +88,11 @@ export const containerItemStats = (containerID: string, currentNode?: string) =>
|
||||
currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
);
|
||||
};
|
||||
export const containerListStats = (currentNode?: string) => {
|
||||
return http.get<Array<Container.ContainerListStats>>(
|
||||
`/containers/list/stats`,
|
||||
{},
|
||||
currentNode ? { headers: { CurrentNode: currentNode } } : {},
|
||||
);
|
||||
export const containerListStats = (currentNode?: string, ids?: string[], signal?: AbortSignal) => {
|
||||
return http.get<Array<Container.ContainerListStats>>(`/containers/list/stats`, ids ? { ids: ids.join(',') } : {}, {
|
||||
headers: currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
export const containerStats = (id: string) => {
|
||||
return http.get<Container.ContainerStats>(`/containers/stats/${id}`);
|
||||
|
||||
@@ -490,7 +490,7 @@ import Uploads from '@/components/upload/index.vue';
|
||||
import DockerStatus from '@/views/container/docker-status/index.vue';
|
||||
import ContainerLogDialog from '@/components/log/container-drawer/index.vue';
|
||||
import Status from '@/components/status/index.vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
|
||||
import {
|
||||
containerItemStats,
|
||||
containerListStats,
|
||||
@@ -703,47 +703,87 @@ const dialogContainerLogRef = ref();
|
||||
const dialogRenameRef = ref();
|
||||
const dialogPruneRef = ref();
|
||||
|
||||
const search = async (column?: any) => {
|
||||
if (!isActive.value || !isExist.value) {
|
||||
let requestController: AbortController | undefined;
|
||||
let requestVersion = 0;
|
||||
let refreshPending = false;
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
requestVersion++;
|
||||
requestController?.abort();
|
||||
});
|
||||
|
||||
const loadContainers = async (background = false, column?: any) => {
|
||||
if (!isActive.value || !isExist.value || (background && refreshPending)) {
|
||||
return;
|
||||
}
|
||||
requestController?.abort();
|
||||
const controller = new AbortController();
|
||||
requestController = controller;
|
||||
const version = ++requestVersion;
|
||||
const isCurrent = () => version === requestVersion && !controller.signal.aborted;
|
||||
refreshPending = true;
|
||||
loading.value = !background;
|
||||
localStorage.setItem('includeAppStore', includeAppStore.value ? 'true' : 'false');
|
||||
let filterItem = (router.currentRoute.value.query?.filters as string) || '';
|
||||
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
|
||||
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
|
||||
let params = {
|
||||
const params = {
|
||||
name: searchName.value,
|
||||
state: paginationConfig.state || 'all',
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
filters: filterItem,
|
||||
filters: props.filters || (router.currentRoute.value.query?.filters as string) || '',
|
||||
orderBy: paginationConfig.orderBy,
|
||||
order: paginationConfig.order,
|
||||
excludeAppStore: !includeAppStore.value,
|
||||
};
|
||||
loading.value = true;
|
||||
const [containerResult, statsResult, statusResult] = await Promise.allSettled([
|
||||
searchContainer(params),
|
||||
containerListStats(),
|
||||
loadContainerStatus(),
|
||||
]);
|
||||
loading.value = false;
|
||||
|
||||
if (containerResult.status === 'fulfilled') {
|
||||
const containers = containerResult.value.data.items || [];
|
||||
// Counts and resource samples must never hold up the table itself.
|
||||
const statusRequest = loadContainerStatus(undefined, true, controller.signal)
|
||||
.then((res) => {
|
||||
if (isCurrent()) {
|
||||
updateTags(res.data || {});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
try {
|
||||
const res = await searchContainer(params, undefined, controller.signal);
|
||||
if (!isCurrent()) {
|
||||
return;
|
||||
}
|
||||
const containers = res.data.items || [];
|
||||
syncContainerRows(containers);
|
||||
paginationConfig.total = containerResult.value.data.total;
|
||||
}
|
||||
|
||||
if (statsResult.status === 'fulfilled') {
|
||||
applyStatsToRows(statsResult.value.data || []);
|
||||
}
|
||||
|
||||
if (statusResult.status === 'fulfilled') {
|
||||
updateTags(statusResult.value.data || {});
|
||||
paginationConfig.total = res.data.total;
|
||||
loading.value = false;
|
||||
// Clear old usage when a previously running container has stopped.
|
||||
applyStatsToRows(
|
||||
containers
|
||||
.filter((item) => item.state !== 'running')
|
||||
.map((item) => ({
|
||||
containerID: item.containerID,
|
||||
...Object.fromEntries(statFields.map((field) => [field, 0])),
|
||||
})),
|
||||
);
|
||||
const ids = containers.filter((item) => item.state === 'running').map((item) => item.containerID);
|
||||
if (ids.length > 0) {
|
||||
const stats = await containerListStats(undefined, ids, controller.signal);
|
||||
if (isCurrent()) {
|
||||
applyStatsToRows(stats.data || []);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// The HTTP layer reports failures; keep the last successful rows visible.
|
||||
} finally {
|
||||
if (isCurrent()) {
|
||||
loading.value = false;
|
||||
}
|
||||
await statusRequest;
|
||||
if (isCurrent()) {
|
||||
refreshPending = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const search = (column?: any) => loadContainers(false, column);
|
||||
|
||||
const searchWithStatus = (item: string) => {
|
||||
activeTag.value = item;
|
||||
paginationConfig.state = activeTag.value;
|
||||
@@ -772,21 +812,7 @@ const changePinned = (row: any, isPinned: boolean) => {
|
||||
});
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
let filterItem = props.filters ? props.filters : '';
|
||||
let params = {
|
||||
name: searchName.value,
|
||||
state: paginationConfig.state || 'all',
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
filters: filterItem,
|
||||
orderBy: paginationConfig.orderBy,
|
||||
order: paginationConfig.order,
|
||||
};
|
||||
const [containerResult, statsResult] = await Promise.all([searchContainer(params), containerListStats()]);
|
||||
syncContainerRows(containerResult.data.items || []);
|
||||
applyStatsToRows(statsResult.data || []);
|
||||
};
|
||||
const refresh = () => loadContainers(true);
|
||||
|
||||
const loadSize = async (row: any) => {
|
||||
containerItemStats(row.containerID).then((res) => {
|
||||
|
||||
Reference in New Issue
Block a user