From 4941e9ec3235be77b50fb4e51c382d7100f7449d Mon Sep 17 00:00:00 2001 From: yusing Date: Thu, 4 Sep 2025 07:30:51 +0800 Subject: [PATCH] feat(docker): implement container management endpoints for start, stop, and restart - Added Restart, Start, and Stop functions to manage Docker containers by ID. - Introduced corresponding request structs (StartRequest, StopRequest) for handling input. - Updated Swagger documentation to include new endpoints and request/response schemas. --- internal/api/handler.go | 3 + internal/api/v1/docker/restart.go | 50 ++++++++ internal/api/v1/docker/start.go | 56 +++++++++ internal/api/v1/docker/stop.go | 56 +++++++++ internal/api/v1/docs/docs.go | 168 +++++++++++++++++++++++++++ internal/api/v1/docs/swagger.json | 187 ++++++++++++++++++++++++++++++ internal/api/v1/docs/swagger.yaml | 122 +++++++++++++++++++ 7 files changed, 642 insertions(+) create mode 100644 internal/api/v1/docker/restart.go create mode 100644 internal/api/v1/docker/start.go create mode 100644 internal/api/v1/docker/stop.go diff --git a/internal/api/handler.go b/internal/api/handler.go index fe2eee35..969a91e3 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -129,6 +129,9 @@ func NewHandler() *gin.Engine { docker.GET("/containers", dockerApi.Containers) docker.GET("/info", dockerApi.Info) docker.GET("/logs/:server/:container", dockerApi.Logs) + docker.POST("/start", dockerApi.Start) + docker.POST("/stop", dockerApi.Stop) + docker.POST("/restart", dockerApi.Restart) } } diff --git a/internal/api/v1/docker/restart.go b/internal/api/v1/docker/restart.go new file mode 100644 index 00000000..14498d12 --- /dev/null +++ b/internal/api/v1/docker/restart.go @@ -0,0 +1,50 @@ +package dockerapi + +import ( + "net/http" + + "github.com/gin-gonic/gin" + apitypes "github.com/yusing/go-proxy/internal/api/types" + "github.com/yusing/go-proxy/internal/docker" +) + +// @x-id "restart" +// @BasePath /api/v1 +// @Summary Restart container +// @Description Restart container by container id +// @Tags docker +// @Produce json +// @Param request body StopRequest true "Request" +// @Success 200 {object} apitypes.SuccessResponse +// @Failure 403 {object} apitypes.ErrorResponse +// @Failure 500 {object} apitypes.ErrorResponse +// @Router /docker/restart [post] +func Restart(c *gin.Context) { + var req StopRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) + return + } + + dockerHost, ok := docker.GetDockerHostByContainerID(req.ID) + if !ok { + c.JSON(http.StatusNotFound, apitypes.Error("container not found")) + return + } + + client, err := docker.NewClient(dockerHost) + if err != nil { + c.Error(apitypes.InternalServerError(err, "failed to create docker client")) + return + } + + defer client.Close() + + err = client.ContainerRestart(c.Request.Context(), req.ID, req.StopOptions) + if err != nil { + c.Error(apitypes.InternalServerError(err, "failed to restart container")) + return + } + + c.JSON(http.StatusOK, apitypes.Success("container restarted")) +} diff --git a/internal/api/v1/docker/start.go b/internal/api/v1/docker/start.go new file mode 100644 index 00000000..a55f9d42 --- /dev/null +++ b/internal/api/v1/docker/start.go @@ -0,0 +1,56 @@ +package dockerapi + +import ( + "net/http" + + "github.com/docker/docker/api/types/container" + "github.com/gin-gonic/gin" + apitypes "github.com/yusing/go-proxy/internal/api/types" + "github.com/yusing/go-proxy/internal/docker" +) + +type StartRequest struct { + ID string `json:"id" binding:"required"` + container.StartOptions +} + +// @x-id "start" +// @BasePath /api/v1 +// @Summary Start container +// @Description Start container by container id +// @Tags docker +// @Produce json +// @Param request body StartRequest true "Request" +// @Success 200 {object} apitypes.SuccessResponse +// @Failure 403 {object} apitypes.ErrorResponse +// @Failure 500 {object} apitypes.ErrorResponse +// @Router /docker/start [post] +func Start(c *gin.Context) { + var req StartRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) + return + } + + dockerHost, ok := docker.GetDockerHostByContainerID(req.ID) + if !ok { + c.JSON(http.StatusNotFound, apitypes.Error("container not found")) + return + } + + client, err := docker.NewClient(dockerHost) + if err != nil { + c.Error(apitypes.InternalServerError(err, "failed to create docker client")) + return + } + + defer client.Close() + + err = client.ContainerStart(c.Request.Context(), req.ID, req.StartOptions) + if err != nil { + c.Error(apitypes.InternalServerError(err, "failed to start container")) + return + } + + c.JSON(http.StatusOK, apitypes.Success("container started")) +} diff --git a/internal/api/v1/docker/stop.go b/internal/api/v1/docker/stop.go new file mode 100644 index 00000000..d47de155 --- /dev/null +++ b/internal/api/v1/docker/stop.go @@ -0,0 +1,56 @@ +package dockerapi + +import ( + "net/http" + + "github.com/docker/docker/api/types/container" + "github.com/gin-gonic/gin" + apitypes "github.com/yusing/go-proxy/internal/api/types" + "github.com/yusing/go-proxy/internal/docker" +) + +type StopRequest struct { + ID string `json:"id" binding:"required"` + container.StopOptions +} + +// @x-id "stop" +// @BasePath /api/v1 +// @Summary Stop container +// @Description Stop container by container id +// @Tags docker +// @Produce json +// @Param request body StopRequest true "Request" +// @Success 200 {object} apitypes.SuccessResponse +// @Failure 403 {object} apitypes.ErrorResponse +// @Failure 500 {object} apitypes.ErrorResponse +// @Router /docker/stop [post] +func Stop(c *gin.Context) { + var req StopRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) + return + } + + dockerHost, ok := docker.GetDockerHostByContainerID(req.ID) + if !ok { + c.JSON(http.StatusNotFound, apitypes.Error("container not found")) + return + } + + client, err := docker.NewClient(dockerHost) + if err != nil { + c.Error(apitypes.InternalServerError(err, "failed to create docker client")) + return + } + + defer client.Close() + + err = client.ContainerStop(c.Request.Context(), req.ID, req.StopOptions) + if err != nil { + c.Error(apitypes.InternalServerError(err, "failed to stop container")) + return + } + + c.JSON(http.StatusOK, apitypes.Success("container stopped")) +} diff --git a/internal/api/v1/docs/docs.go b/internal/api/v1/docs/docs.go index 3da3bf2b..86a46ba1 100644 --- a/internal/api/v1/docs/docs.go +++ b/internal/api/v1/docs/docs.go @@ -575,6 +575,138 @@ const docTemplate = `{ "x-id": "logs" } }, + "/docker/restart": { + "post": { + "description": "Restart container by container id", + "produces": [ + "application/json" + ], + "tags": [ + "docker" + ], + "summary": "Restart container", + "parameters": [ + { + "description": "Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dockerapi.StopRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/SuccessResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "x-id": "restart" + } + }, + "/docker/start": { + "post": { + "description": "Start container by container id", + "produces": [ + "application/json" + ], + "tags": [ + "docker" + ], + "summary": "Start container", + "parameters": [ + { + "description": "Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dockerapi.StartRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/SuccessResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "x-id": "start" + } + }, + "/docker/stop": { + "post": { + "description": "Stop container by container id", + "produces": [ + "application/json" + ], + "tags": [ + "docker" + ], + "summary": "Stop container", + "parameters": [ + { + "description": "Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dockerapi.StopRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/SuccessResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "x-id": "stop" + } + }, "/favicon": { "get": { "description": "Get favicon", @@ -3586,6 +3718,42 @@ const docTemplate = `{ } } }, + "dockerapi.StartRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "checkpointDir": { + "type": "string" + }, + "checkpointID": { + "type": "string" + }, + "id": { + "type": "string" + } + } + }, + "dockerapi.StopRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + }, + "signal": { + "description": "Signal (optional) is the signal to send to the container to (gracefully)\nstop it before forcibly terminating the container with SIGKILL after the\ntimeout expires. If not value is set, the default (SIGTERM) is used.", + "type": "string" + }, + "timeout": { + "description": "Timeout (optional) is the timeout (in seconds) to wait for the container\nto stop gracefully before forcibly terminating it with SIGKILL.\n\n- Use nil to use the default timeout (10 seconds).\n- Use '-1' to wait indefinitely.\n- Use '0' to not wait for the container to exit gracefully, and\n immediately proceeds to forcibly terminating the container.\n- Other positive values are used as timeout (in seconds).", + "type": "integer" + } + } + }, "homepage.FetchResult": { "type": "object", "properties": { diff --git a/internal/api/v1/docs/swagger.json b/internal/api/v1/docs/swagger.json index d9b8b827..e4a2bb5a 100644 --- a/internal/api/v1/docs/swagger.json +++ b/internal/api/v1/docs/swagger.json @@ -581,6 +581,141 @@ "operationId": "logs" } }, + "/docker/restart": { + "post": { + "description": "Restart container by container id", + "produces": [ + "application/json" + ], + "tags": [ + "docker" + ], + "summary": "Restart container", + "parameters": [ + { + "description": "Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dockerapi.StopRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/SuccessResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "x-id": "restart", + "operationId": "restart" + } + }, + "/docker/start": { + "post": { + "description": "Start container by container id", + "produces": [ + "application/json" + ], + "tags": [ + "docker" + ], + "summary": "Start container", + "parameters": [ + { + "description": "Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dockerapi.StartRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/SuccessResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "x-id": "start", + "operationId": "start" + } + }, + "/docker/stop": { + "post": { + "description": "Stop container by container id", + "produces": [ + "application/json" + ], + "tags": [ + "docker" + ], + "summary": "Stop container", + "parameters": [ + { + "description": "Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dockerapi.StopRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/SuccessResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "x-id": "stop", + "operationId": "stop" + } + }, "/favicon": { "get": { "description": "Get favicon", @@ -4281,6 +4416,58 @@ "x-nullable": false, "x-omitempty": false }, + "dockerapi.StartRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "checkpointDir": { + "type": "string", + "x-nullable": false, + "x-omitempty": false + }, + "checkpointID": { + "type": "string", + "x-nullable": false, + "x-omitempty": false + }, + "id": { + "type": "string", + "x-nullable": false, + "x-omitempty": false + } + }, + "x-nullable": false, + "x-omitempty": false + }, + "dockerapi.StopRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "x-nullable": false, + "x-omitempty": false + }, + "signal": { + "description": "Signal (optional) is the signal to send to the container to (gracefully)\nstop it before forcibly terminating the container with SIGKILL after the\ntimeout expires. If not value is set, the default (SIGTERM) is used.", + "type": "string", + "x-nullable": false, + "x-omitempty": false + }, + "timeout": { + "description": "Timeout (optional) is the timeout (in seconds) to wait for the container\nto stop gracefully before forcibly terminating it with SIGKILL.\n\n- Use nil to use the default timeout (10 seconds).\n- Use '-1' to wait indefinitely.\n- Use '0' to not wait for the container to exit gracefully, and\n immediately proceeds to forcibly terminating the container.\n- Other positive values are used as timeout (in seconds).", + "type": "integer", + "x-nullable": false, + "x-omitempty": false + } + }, + "x-nullable": false, + "x-omitempty": false + }, "homepage.FetchResult": { "type": "object", "properties": { diff --git a/internal/api/v1/docs/swagger.yaml b/internal/api/v1/docs/swagger.yaml index 5010cd16..6351f433 100644 --- a/internal/api/v1/docs/swagger.yaml +++ b/internal/api/v1/docs/swagger.yaml @@ -1097,6 +1097,41 @@ definitions: used_percent: type: number type: object + dockerapi.StartRequest: + properties: + checkpointDir: + type: string + checkpointID: + type: string + id: + type: string + required: + - id + type: object + dockerapi.StopRequest: + properties: + id: + type: string + signal: + description: |- + Signal (optional) is the signal to send to the container to (gracefully) + stop it before forcibly terminating the container with SIGKILL after the + timeout expires. If not value is set, the default (SIGTERM) is used. + type: string + timeout: + description: |- + Timeout (optional) is the timeout (in seconds) to wait for the container + to stop gracefully before forcibly terminating it with SIGKILL. + + - Use nil to use the default timeout (10 seconds). + - Use '-1' to wait indefinitely. + - Use '0' to not wait for the container to exit gracefully, and + immediately proceeds to forcibly terminating the container. + - Other positive values are used as timeout (in seconds). + type: integer + required: + - id + type: object homepage.FetchResult: properties: errMsg: @@ -1747,6 +1782,93 @@ paths: - docker - websocket x-id: logs + /docker/restart: + post: + description: Restart container by container id + parameters: + - description: Request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dockerapi.StopRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/SuccessResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Restart container + tags: + - docker + x-id: restart + /docker/start: + post: + description: Start container by container id + parameters: + - description: Request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dockerapi.StartRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/SuccessResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Start container + tags: + - docker + x-id: start + /docker/stop: + post: + description: Stop container by container id + parameters: + - description: Request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dockerapi.StopRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/SuccessResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Stop container + tags: + - docker + x-id: stop /favicon: get: consumes: