package docker import ( "context" "encoding/json" "fmt" "io" "os" "strings" "time" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/global" "github.com/docker/cli/cli/config" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "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/registry" "github.com/docker/docker/client" ) func NewDockerClient() (*client.Client, error) { var settingItem model.Setting _ = global.DB.Where("key = ?", "DockerSockPath").First(&settingItem).Error if len(settingItem.Value) == 0 { settingItem.Value = "unix:///var/run/docker.sock" } cli, err := client.NewClientWithOpts(client.FromEnv, client.WithHost(settingItem.Value), client.WithAPIVersionNegotiation()) if err != nil { return nil, err } return cli, nil } func NewClient() (Client, error) { var settingItem model.Setting _ = global.DB.Where("key = ?", "DockerSockPath").First(&settingItem).Error if len(settingItem.Value) == 0 { settingItem.Value = "unix:///var/run/docker.sock" } cli, err := client.NewClientWithOpts(client.FromEnv, client.WithHost(settingItem.Value), client.WithAPIVersionNegotiation()) if err != nil { return Client{}, err } return Client{ cli: cli, }, nil } func NewClientWithExist(cli *client.Client) Client { return Client{ cli: cli, } } type Client struct { cli *client.Client } func (c Client) Close() { _ = c.cli.Close() } func (c Client) ListContainersByName(names []string) ([]container.Summary, error) { var ( options container.ListOptions namesMap = make(map[string]bool) res []container.Summary ) options.All = true if len(names) > 0 { var array []filters.KeyValuePair for _, n := range names { namesMap["/"+n] = true array = append(array, filters.Arg("name", n)) } options.Filters = filters.NewArgs(array...) } containers, err := c.cli.ContainerList(context.Background(), options) if err != nil { return nil, err } for _, con := range containers { if _, ok := namesMap[con.Names[0]]; ok { res = append(res, con) } } return res, nil } func (c Client) ListAllContainers() ([]container.Summary, error) { var ( options container.ListOptions ) options.All = true containers, err := c.cli.ContainerList(context.Background(), options) if err != nil { return nil, err } return containers, nil } func (c Client) CreateNetwork(name string) error { _, err := c.cli.NetworkCreate(context.Background(), name, network.CreateOptions{ Driver: "bridge", EnableIPv6: new(bool), }) return err } func (c Client) DeleteImage(imageID string) error { if _, err := c.cli.ImageRemove(context.Background(), imageID, image.RemoveOptions{Force: true}); err != nil { return err } return nil } func (c Client) ImageExists(imageID string) (bool, error) { _, err := c.cli.ImageInspect(context.Background(), imageID) if err != nil { return false, err } return true, nil } func (c Client) GetImageIDByName(imageName string) (string, error) { filter := filters.NewArgs() filter.Add("reference", imageName) list, err := c.cli.ImageList(context.Background(), image.ListOptions{ Filters: filter, }) if err != nil { return "", err } if len(list) > 0 { return list[0].ID, nil } return "", nil } func (c Client) NetworkExist(name string) bool { var options network.ListOptions options.Filters = filters.NewArgs(filters.Arg("name", name)) networks, err := c.cli.NetworkList(context.Background(), options) if err != nil { return false } return len(networks) > 0 } func CreateDefaultDockerNetwork() error { cli, err := NewClient() if err != nil { global.LOG.Warnf("init docker client error %s", err.Error()) return err } defer cli.Close() if !cli.NetworkExist("1panel-network") { if err := cli.CreateNetwork("1panel-network"); err != nil { global.LOG.Warnf("create default docker network error %s", err.Error()) return err } } return nil } func setLog(id, newLastLine string, task *task.Task) error { data, err := os.ReadFile(task.Task.LogFile) if err != nil { return fmt.Errorf("failed to read file: %v", err) } lines := strings.Split(string(data), "\n") exist := false for index, line := range lines { if strings.Contains(line, id) { timeStr := time.Now().Format("2006/01/02 15:04:05") lines[index] = timeStr + " " + newLastLine exist = true break } else { lines[index] = strings.TrimSpace(lines[index]) } } if !exist { task.Log(newLastLine) return nil } output := strings.Join(lines, "\n") _ = os.WriteFile(task.Task.LogFile, []byte(output), os.ModePerm) return nil } func (c Client) PullImageWithProcessAndOptions(task *task.Task, imageName string, options image.PullOptions) error { out, err := c.cli.ImagePull(context.Background(), imageName, options) if err != nil { return err } defer out.Close() return handlePullImageProcess(out, task) } func handlePullImageProcess(out io.Reader, task *task.Task) error { decoder := json.NewDecoder(out) for { var progress map[string]interface{} if err := decoder.Decode(&progress); err != nil { if err == io.EOF { break } return err } if msg, ok := progress["errorDetail"]; ok { return fmt.Errorf("image pull failed, err: %v", msg) } if msg, ok := progress["error"]; ok { return fmt.Errorf("image pull failed, err: %v", msg) } if task == nil { continue } status, _ := progress["status"].(string) switch status { case "Downloading", "Extracting": logProcess(progress, task) case "Pull complete", "Download complete", "Already exists", "Verifying Checksum": id, _ := progress["id"].(string) progressStr := fmt.Sprintf("%s %s", status, id) _ = setLog(id, progressStr, task) } } return nil } func (c Client) PushImageWithProcessAndOptions(task *task.Task, imageName string, options image.PushOptions) error { out, err := c.cli.ImagePush(context.Background(), imageName, options) if err != nil { return err } defer out.Close() decoder := json.NewDecoder(out) for { var progress map[string]interface{} if err = decoder.Decode(&progress); err != nil { if err == io.EOF { break } return err } if msg, ok := progress["errorDetail"]; ok { return fmt.Errorf("image push failed, err: %v", msg) } if msg, ok := progress["error"]; ok { return fmt.Errorf("image push failed, err: %v", msg) } status, _ := progress["status"].(string) switch status { case "Pushing": logProcess(progress, task) case "Pushed": id, _ := progress["id"].(string) progressStr := fmt.Sprintf("%s %s", status, id) _ = setLog(id, progressStr, task) default: progressStr, _ := json.Marshal(progress) task.Log(string(progressStr)) } } return nil } func (c Client) BuildImageWithProcessAndOptions(task *task.Task, tar io.ReadCloser, options types.ImageBuildOptions) error { out, err := c.cli.ImageBuild(context.Background(), tar, options) if err != nil { return err } defer out.Body.Close() decoder := json.NewDecoder(out.Body) for { var progress map[string]interface{} if err = decoder.Decode(&progress); err != nil { if err == io.EOF { break } return err } if msg, ok := progress["errorDetail"]; ok { return fmt.Errorf("image build failed, err: %v", msg) } if msg, ok := progress["error"]; ok { return fmt.Errorf("image build failed, err: %v", msg) } status, _ := progress["status"].(string) stream, _ := progress["stream"].(string) if len(status) == 0 && len(stream) != 0 { if stream != "\n" { task.Log(stream) } continue } switch status { case "Downloading", "Extracting": logProcess(progress, task) case "Pull complete", "Download complete", "Verifying Checksum": id, _ := progress["id"].(string) progressStr := fmt.Sprintf("%s %s", status, id) _ = setLog(id, progressStr, task) default: progressStr, _ := json.Marshal(progress) task.Log(string(progressStr)) } } return nil } func (c Client) PullImageWithProcess(task *task.Task, imageName string) error { options := image.PullOptions{} if authStr, ok := loadRegistryAuthFromDockerConfig(imageName); ok { options.RegistryAuth = authStr } return c.PullImageWithProcessAndOptions(task, imageName, options) } func logProcess(progress map[string]interface{}, task *task.Task) { status, _ := progress["status"].(string) id, _ := progress["id"].(string) progressItem, _ := progress["progress"].(string) progressStr := "" progressStr = fmt.Sprintf("%s %s %s", status, id, progressItem) _ = setLog(id, progressStr, task) } func PullImage(imageName string) error { cli, err := NewDockerClient() if err != nil { return err } defer cli.Close() options := image.PullOptions{} if authStr, ok := loadRegistryAuthFromDockerConfig(imageName); ok { options.RegistryAuth = authStr } if _, err := cli.ImagePull(context.Background(), imageName, options); err != nil { return err } return nil } func loadRegistryAuthFromDockerConfig(imageName string) (string, bool) { registryHost, hasRegistry := extractRegistryHost(imageName) cfg := config.LoadDefaultConfigFile(io.Discard) if cfg == nil { return "", false } candidates := make([]string, 0) if hasRegistry { candidates = append(candidates, registryHost, "https://"+registryHost, "http://"+registryHost) } if !hasRegistry || isDockerHubRegistry(registryHost) { candidates = append(candidates, "https://index.docker.io/v1/", "index.docker.io", "docker.io", "registry-1.docker.io", "https://registry-1.docker.io", ) } seen := make(map[string]struct{}) for _, key := range candidates { if key == "" { continue } if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} auth, err := cfg.GetAuthConfig(key) if err != nil { continue } if auth.Username == "" && auth.Password == "" && auth.Auth == "" && auth.IdentityToken == "" && auth.RegistryToken == "" { continue } authStr, err := registry.EncodeAuthConfig(registry.AuthConfig{ Username: auth.Username, Password: auth.Password, Auth: auth.Auth, ServerAddress: auth.ServerAddress, IdentityToken: auth.IdentityToken, RegistryToken: auth.RegistryToken, }) if err != nil { return "", false } return authStr, true } return "", false } func isDockerHubRegistry(host string) bool { switch normalizeRegistryHost(host) { case "docker.io", "index.docker.io", "registry-1.docker.io": return true default: return false } } func extractRegistryHost(imageName string) (string, bool) { parts := strings.Split(imageName, "/") if len(parts) < 2 { return "", false } first := parts[0] if strings.Contains(first, ".") || strings.Contains(first, ":") || first == "localhost" { return normalizeRegistryHost(first), true } return "", false } func normalizeRegistryHost(registryKey string) string { key := strings.TrimSpace(registryKey) if key == "" { return "" } key = strings.TrimPrefix(key, "http://") key = strings.TrimPrefix(key, "https://") key = strings.Trim(key, "/") if strings.Contains(key, "/") { key = strings.SplitN(key, "/", 2)[0] } return strings.ToLower(key) }