Repository: 1Panel-dev/1Panel
Pull request #8200: fix: Modify the loading mode of container construction and push log
Author: login: ssongliu
Labels: name: do-not-merge/release-note-label-needed
name: approved
name: dev-v2
Created: 2025-03-20T02:59:18Z | Merged: 2025-03-20T03:01:40Z



Diff:
diff --git a/agent/app/service/image.go b/agent/app/service/image.go
index adfff0779409..4ada15fdbf59 100644
--- a/agent/app/service/image.go
+++ b/agent/app/service/image.go
@@ -203,17 +203,10 @@ func (u *ImageService) ImageBuild(req dto.ImageBuild) error {
 	go func() {
 		defer tar.Close()
 		taskItem.AddSubTask(i18n.GetMsgByKey("ImageBuild"), func(t *task.Task) error {
-			res, err := client.ImageBuild(context.Background(), tar, opts)
-			if err != nil {
-				return err
-			}
-			defer res.Body.Close()
-			body, _ := io.ReadAll(res.Body)
-			if strings.Contains(string(body), "errorDetail") || strings.Contains(string(body), "error:") {
-				taskItem.LogWithStatus(i18n.GetMsgByKey("ImageBuildStdoutCheck"), fmt.Errorf("build image %s failed", req.Name))
+			dockerCli := docker.NewClientWithExist(client)
+			if err := dockerCli.BuildImageWithProcessAndOptions(taskItem, tar, opts); err != nil {
 				return err
 			}
-			taskItem.LogSuccess(i18n.GetWithName("ImaegBuildRes", "\n"+string(body)))
 			return nil
 		}, nil)
 
@@ -381,13 +374,10 @@ func (u *ImageService) ImagePush(req dto.ImagePush) error {
 			return nil
 		}, nil)
 		taskItem.AddSubTask(i18n.GetMsgByKey("TaskPush"), func(t *task.Task) error {
-			out, err := client.ImagePush(context.TODO(), newName, options)
-			if err != nil {
+			dockerCli := docker.NewClientWithExist(client)
+			if err := dockerCli.PushImageWithProcessAndOptions(taskItem, newName, options); err != nil {
 				return err
 			}
-			defer out.Close()
-			body, _ := io.ReadAll(out)
-			taskItem.Log(i18n.GetWithName("ImaegPushRes", "\n"+string(body)))
 			return nil
 		}, nil)
 		_ = taskItem.Execute()
diff --git a/agent/utils/docker/docker.go b/agent/utils/docker/docker.go
index 852f4a21c6bd..d30504e5c7fa 100644
--- a/agent/utils/docker/docker.go
+++ b/agent/utils/docker/docker.go
@@ -4,6 +4,11 @@ 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"
@@ -13,10 +18,6 @@ import (
 	"github.com/docker/docker/api/types/image"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/client"
-	"io"
-	"os"
-	"strings"
-	"time"
 )
 
 func NewDockerClient() (*client.Client, error) {
@@ -220,6 +221,111 @@ func (c Client) PullImageWithProcessAndOptions(task *task.Task, imageName string
 	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)
+		}
+		timeStr := time.Now().Format("2006/01/02 15:04:05")
+		status, _ := progress["status"].(string)
+		switch status {
+		case "Pushing":
+			id, _ := progress["id"].(string)
+			progressDetail, _ := progress["progressDetail"].(map[string]interface{})
+			current, _ := progressDetail["current"].(float64)
+			progressStr := ""
+			total, ok := progressDetail["total"].(float64)
+			if ok {
+				progressStr = fmt.Sprintf("%s %s [%s] --- %.2f%%", timeStr, status, id, (current/total)*100)
+			} else {
+				progressStr = fmt.Sprintf("%s %s [%s] --- %.2f%%", timeStr, status, id, current)
+			}
+
+			_ = setLog(id, progressStr, task)
+		case "Pushed":
+			id, _ := progress["id"].(string)
+			progressStr := fmt.Sprintf("%s %s [%s] --- %.2f%%", timeStr, status, id, 100.0)
+			_ = 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)
+		}
+		timeStr := time.Now().Format("2006/01/02 15:04:05")
+		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":
+			id, _ := progress["id"].(string)
+			progressDetail, _ := progress["progressDetail"].(map[string]interface{})
+			current, _ := progressDetail["current"].(float64)
+			progressStr := ""
+			total, ok := progressDetail["total"].(float64)
+			if ok {
+				progressStr = fmt.Sprintf("%s %s [%s] --- %.2f%%", timeStr, status, id, (current/total)*100)
+			} else {
+				progressStr = fmt.Sprintf("%s %s [%s] --- %.2f%%", timeStr, status, id, current)
+			}
+
+			_ = setLog(id, progressStr, task)
+		case "Pull complete", "Download complete", "Verifying Checksum":
+			id, _ := progress["id"].(string)
+			progressStr := fmt.Sprintf("%s %s [%s] --- %.2f%%", timeStr, status, id, 100.0)
+			_ = 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 {
 	return c.PullImageWithProcessAndOptions(task, imageName, image.PullOptions{})
 }
diff --git a/frontend/src/components/status/index.vue b/frontend/src/components/status/index.vue
index 77e3366a93f2..799e3a1dc137 100644
--- a/frontend/src/components/status/index.vue
+++ b/frontend/src/components/status/index.vue
@@ -55,7 +55,7 @@ const getType = (status: string) => {
         case 'enable':
         case 'done':
         case 'healthy':
-        case 'used':
+        case 'unused':
         case 'executing':
             return 'success';
         case 'stopped':


Reviews:
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T02:59:34Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T02:59:44Z
user: login: wanghe-fit2cloud
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T02:59:47Z
user: login: wanghe-fit2cloud
body: /lgtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T03:01:21Z

Review comments:
diff_hunk: @@ -220,6 +221,111 @@ func (c Client) PullImageWithProcessAndOptions(task *task.Task, imageName string
 	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)
+		}
+		timeStr := time.Now().Format("2006/01/02 15:04:05")
+		status, _ := progress["status"].(string)
+		switch status {
+		case "Pushing":
+			id, _ := progress["id"].(string)
+			progressDetail, _ := progress["progressDetail"].(map[string]interface{})
+			current, _ := progressDetail["current"].(float64)
+			progressStr := ""
+			total, ok := progressDetail["total"].(float64)
+			if ok {
+				progressStr = fmt.Sprintf("%s %s [%s] --- %.2f%%", timeStr, status, id, (current/total)*100)
+			} else {
+				progressStr = fmt.Sprintf("%s %s [%s] --- %.2f%%", timeStr, status, id, current)
+			}
+
+			_ = setLog(id, progressStr, task)
+		case "Pushed":
+			id, _ := progress["id"].(string)
+			progressStr := fmt.Sprintf("%s %s [%s] --- %.2f%%", timeStr, status, id, 100.0)
+			_ = 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)
+		}
+		timeStr := time.Now().Format("2006/01/02 15:04:05")
+		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":
+			id, _ := progress["id"].(string)
+			progressDetail, _ := progress["progressDetail"].(map[string]interface{})
+			current, _ := progressDetail["current"].(float64)
+			progressStr := ""
+			total, ok := progressDetail["total"].(float64)
+			if ok {
+				progressStr = fmt.Sprintf("%s %s [%s] --- %.2f%%", timeStr, status, id, (current/total)*100)
+			} else {
+				progressStr = fmt.Sprintf("%s %s [%s] --- %.2f%%", timeStr, status, id, current)
+			}
+
+			_ = setLog(id, progressStr, task)
+		case "Pull complete", "Download complete", "Verifying Checksum":
+			id, _ := progress["id"].(string)
+			progressStr := fmt.Sprintf("%s %s [%s] --- %.2f%%", timeStr, status, id, 100.0)
+			_ = 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 {
 	return c.PullImageWithProcessAndOptions(task, imageName, image.PullOptions{})
 }
path: agent/utils/docker/docker.go
user: login: wanghe-fit2cloud
body: ### Code Differences:

- **Imports**: There are small additions to handle I/O operations (`io`, `os`, `strings`, and `time`) at the top of several files, which is generally good practice for handling file operations and time-related tasks.

- **PushImageWithProcessAndOptions**:
  - The function now reads from `out` instead of using an uninitialized variable.
  - It uses JSON decoding to decode each progress update received from Docker.
  - It checks if the `errorDetail` or `error` keys are present in the response to provide more informative error messages.
  - It formats progress updates with timestamps, statuses, IDs, percentages, etc., and logs them using `setLog`.

- **BuildImageWithProcessAndOptions**:
  - Similar to `PushImageWithProcessAndOptions`, it reads from `out.Body`.
  - It also decodes JSON progress updates.
  - It handles different stages of the build process (downloading, extracting, verifying checksum), logging relevant information with a timestamp.

In general, these changes enhance robustness and clarity in tracking progress during API calls involving images with Docker client. They make error handling more specific and log output more detailed. While minor, these improvements help ensure that tasks can be monitored effectively across various phases of image management.
created_at: 2025-03-20T02:59:34Z
author_association: MEMBER
diff_hunk: @@ -381,13 +374,10 @@ func (u *ImageService) ImagePush(req dto.ImagePush) error {
 			return nil
 		}, nil)
 		taskItem.AddSubTask(i18n.GetMsgByKey("TaskPush"), func(t *task.Task) error {
-			out, err := client.ImagePush(context.TODO(), newName, options)
-			if err != nil {
+			dockerCli := docker.NewClientWithExist(client)
+			if err := dockerCli.PushImageWithProcessAndOptions(taskItem, newName, options); err != nil {
 				return err
 			}
-			defer out.Close()
-			body, _ := io.ReadAll(out)
-			taskItem.Log(i18n.GetWithName("ImaegPushRes", "\n"+string(body)))
 			return nil
 		}, nil)
 		_ = taskItem.Execute()
path: agent/app/service/image.go
user: login: wanghe-fit2cloud
body: Here are the main changes and optimizations suggested for the provided code snippets:

---

**Changes to `ImageBuild` method:**

1. **Simplified Error Handling:**
   - Removed manual error checking within the goroutine's sub-task function.
   - Used `defer res.Close()` directly after creating `res`.

2. **Directly Read Response Data:**
   - Replaced reading body content into memory with `ioutil.ReadAll`. This is more efficient but consumes more temporary memory.

3. **Used Docker Client Properly:**
   - Replaced use of raw HTTP client with a proper Docker client (`docker.NewClientWithExist(client)`).
   - Utilized new methods like `dockerCli.BuildImageWithProcessAndOptions`.

**Suggestions for Code Improvement:** 

1. **Error Checking at Function Boundaries:**
   - Add more detailed logging or errors at boundaries where tasks start execution, such as before starting the goroutines. This helps in tracing failures easier.

2. **Avoid Memory Leaks:**
   - Ensure all resources are cleaned up properly using deferred calls (`defer`). However, this has already been effectively handled here based on standard practices in Go.

3. **Consider Resource Limits:**
   - For larger builds, consider setting limits on resource usage such as disk space and CPU time to prevent excessive load on system resources.

4. **Logging Consistency with Status Messages:**
   - Maintain consistency between successful log messages and status messages during failure scenarios.

---

**Changes to `ImagePush` method:**

1. **Removed Unused Variable:**
   - Removed redundant assignment `options := req.Options` since it isn't needed within the function.

2. **Refactored Task Execution Logic:**
   - Simplified task execution by removing intermediate steps that didn’t need to be managed through additional functions or context variables (`go func() {}`, `t.Run(...)`).

3. **Directly Handle Output Stream:**
   - Directly read response stream data instead of manually copying into memory (`io.ReadAll(out)`). Similar to `ImageBuild`, ensures better performance for handling large responses.

**Suggestions for Future Enhancements:**

1. **Asynchronous Logging:**
   - Consider asynchronous logging mechanisms if logging becomes too verbose or if you want to capture logs from multiple tasks simultaneously.

2. **Custom Error Responses:**
   - Develop custom error response formats to enhance readability and ease of debugging when working with complex build/push operations.

3. **Performance Monitoring Integration:**
   - Integrate monitoring tools to track the progress and duration of each build/push operation. This can help identify bottlenecks early.

By implementing these improvements, the code should become more robust, readable, and maintainable while maintaining its functionality efficiently.
created_at: 2025-03-20T02:59:44Z
author_association: MEMBER
diff_hunk: @@ -55,7 +55,7 @@ const getType = (status: string) => {
         case 'enable':
         case 'done':
         case 'healthy':
-        case 'used':
+        case 'unused':
         case 'executing':
             return 'success';
         case 'stopped':
path: frontend/src/components/status/index.vue
user: login: wanghe-fit2cloud
body: There appears to be only one change:
1. The word "used" has been replaced with "unused".
created_at: 2025-03-20T02:59:47Z
author_association: MEMBER

PR comments:
user: login: f2c-ci-robot[bot]
created_at: 2025-03-20T02:59:21Z
body: Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our [release note process](https://git.k8s.io/community/contributors/guide/release-notes.md) to remove it.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-20T03:00:59Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=1Panel-dev_1Panel&pullRequest=8200) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [48.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=1Panel-dev_1Panel&pullRequest=8200&metric=new_duplicated_lines_density&view=list) (required ≤ 10%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=1Panel-dev_1Panel&pullRequest=8200)


author_association: NONE
user: login: wanghe-fit2cloud
created_at: 2025-03-20T03:01:34Z
body: /approve
author_association: MEMBER
user: login: f2c-ci-robot[bot]
created_at: 2025-03-20T03:01:40Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/1Panel-dev/1Panel/pull/8200#issuecomment-2739006768" title="Approved">wanghe-fit2cloud</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=1Panel-dev%2F1Panel).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/1Panel-dev/1Panel/blob/dev-v2/OWNERS)~~ [wanghe-fit2cloud]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE