Files
panel/internal/data/container_image.go
T
耗子andClaude Opus 5 fe7cce6201 refactor: 对齐 libtnb/chi-skeleton 最新脚手架
- 依赖注入换到 libtnb/wire v0.3.0,各层包导出 Module 并逐类型 Export,
  wire.Struct 改用 Struct[T](),cleanup 签名改为 func() error
- 测试断言换到 libtnb/assert 的 must/check,20 个 testify suite 拆成标准测试函数
- mock 改用 matryer 模板输出到 internal/mocks,wire 走 go.mod tool 指令,
  mockery 用 go run 调用,避免它的依赖树污染 go.mod
- 引入 .golangci.yml 并修完全部告警,darwin 与 linux 双平台 0 issues
- 工作流改用 go-version-file、govulncheck 官方 action,
  wire 校验移入 test.yml,mockery 由自动提交改为 PR 校验

顺带修复:pkg/db 四处漏查 rows.Err()(DatabaseExists 查询出错会被当成库不存在)、
websitestat 负值转 uint64 污染流量统计、pty 终端尺寸超 uint16 截断、
文件权限位超 32 位静默截断、面板与 ACME challenge 服务器缺 ReadHeaderTimeout、
GetDefault 丢失操作人、三处对结构体值恒成立的假断言、两处带空格而失效的 nolint

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:36:21 +08:00

143 lines
3.5 KiB
Go

package data
import (
"context"
"encoding/base64"
"encoding/json"
"slices"
"strings"
"time"
cerrdefs "github.com/containerd/errdefs"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/client"
"github.com/samber/lo"
"github.com/acepanel/panel/v3/internal/biz"
"github.com/acepanel/panel/v3/internal/request"
"github.com/acepanel/panel/v3/pkg/tools"
"github.com/acepanel/panel/v3/pkg/types"
)
type containerImageRepo struct{}
func NewContainerImageRepo() biz.ContainerImageRepo {
return &containerImageRepo{}
}
// List 列出镜像
func (r *containerImageRepo) List(sock string) ([]types.ContainerImage, error) {
apiClient, err := getDockerClient(sock)
if err != nil {
return nil, err
}
defer func(apiClient *client.Client) { _ = apiClient.Close() }(apiClient)
resp, err := apiClient.ImageList(context.Background(), client.ImageListOptions{
All: true,
})
if err != nil {
return nil, err
}
images := lo.Map(resp.Items, func(item image.Summary, _ int) types.ContainerImage {
return types.ContainerImage{
ID: item.ID,
Containers: item.Containers,
RepoTags: item.RepoTags,
RepoDigests: item.RepoDigests,
Size: tools.FormatBytes(float64(item.Size)),
Labels: types.MapToKV(item.Labels),
CreatedAt: time.Unix(item.Created, 0),
}
})
slices.SortFunc(images, func(a types.ContainerImage, b types.ContainerImage) int {
return strings.Compare(a.ID, b.ID)
})
return images, nil
}
// Exist 检查镜像是否存在
func (r *containerImageRepo) Exist(sock string, name string) (bool, error) {
apiClient, err := getDockerClient(sock)
if err != nil {
return false, err
}
defer func(apiClient *client.Client) { _ = apiClient.Close() }(apiClient)
_, err = apiClient.ImageInspect(context.Background(), name)
if err != nil {
if cerrdefs.IsNotFound(err) {
return false, nil
}
return false, err
}
return true, nil
}
// Pull 拉取镜像
func (r *containerImageRepo) Pull(sock string, req *request.ContainerImagePull) error {
apiClient, err := getDockerClient(sock)
if err != nil {
return err
}
defer func(apiClient *client.Client) { _ = apiClient.Close() }(apiClient)
options := client.ImagePullOptions{}
if req.Auth {
authConfig := registry.AuthConfig{
Username: req.Username,
Password: req.Password,
}
encodedJSON, err := json.Marshal(authConfig) //nolint:gosec
if err != nil {
return err
}
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
options.RegistryAuth = authStr
}
out, err := apiClient.ImagePull(context.Background(), req.Name, options)
if err != nil {
return err
}
defer func(out client.ImagePullResponse) { _ = out.Close() }(out)
return out.Wait(context.Background())
}
// Remove 删除镜像
func (r *containerImageRepo) Remove(sock string, id string) error {
apiClient, err := getDockerClient(sock)
if err != nil {
return err
}
defer func(apiClient *client.Client) { _ = apiClient.Close() }(apiClient)
_, err = apiClient.ImageRemove(context.Background(), id, client.ImageRemoveOptions{
Force: true,
PruneChildren: true,
})
return err
}
// Prune 清理未使用的镜像
func (r *containerImageRepo) Prune(sock string) error {
apiClient, err := getDockerClient(sock)
if err != nil {
return err
}
defer func(apiClient *client.Client) { _ = apiClient.Close() }(apiClient)
_, err = apiClient.ImagePrune(context.Background(), client.ImagePruneOptions{
Filters: make(client.Filters).
Add("dangling", "false").
Add("label", "created_by!=acepanel"),
})
return err
}