From 83976646db076269f47dd1c6e9cb7861fb3a23a6 Mon Sep 17 00:00:00 2001 From: yusing Date: Sun, 25 Jan 2026 19:55:11 +0800 Subject: [PATCH] feat(api): support query parameters for proxmox journalctl endpoint Refactored the journalctl API to accept `node`, `vmid`, and `service` parameters as query strings in addition to path parameters. Added a new route `/proxmox/journalctl` that accepts all parameters via query string while maintaining backward compatibility with existing path-parameter routes. - Changed `JournalctlRequest` struct binding from URI-only to query+URI - Simplified Swagger documentation by consolidating multiple route definitions - Existing path-parameter routes remain functional for backward compatibility --- internal/api/handler.go | 1 + internal/api/v1/docs/swagger.json | 180 +++++++++++++++++++++++--- internal/api/v1/docs/swagger.yaml | 137 +++++++++++++++++--- internal/api/v1/proxmox/journalctl.go | 30 ++--- 4 files changed, 300 insertions(+), 48 deletions(-) diff --git a/internal/api/handler.go b/internal/api/handler.go index 6e65e7ad..f6b11e97 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -142,6 +142,7 @@ func NewHandler(requireAuth bool) *gin.Engine { proxmox := v1.Group("/proxmox") { + proxmox.GET("/journalctl", proxmoxApi.Journalctl) proxmox.GET("/journalctl/:node", proxmoxApi.Journalctl) proxmox.GET("/journalctl/:node/:vmid", proxmoxApi.Journalctl) proxmox.GET("/journalctl/:node/:vmid/:service", proxmoxApi.Journalctl) diff --git a/internal/api/v1/docs/swagger.json b/internal/api/v1/docs/swagger.json index f1aeafc2..3636fbbe 100644 --- a/internal/api/v1/docs/swagger.json +++ b/internal/api/v1/docs/swagger.json @@ -2077,6 +2077,86 @@ "operationId": "uptime" } }, + "/proxmox/journalctl": { + "get": { + "description": "Get journalctl output for node or LXC container. If vmid is not provided, streams node journalctl.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "proxmox", + "websocket" + ], + "summary": "Get journalctl output", + "parameters": [ + { + "maximum": 1000, + "minimum": 1, + "type": "integer", + "default": 100, + "description": "Limit output lines (1-1000)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)", + "name": "service", + "in": "query" + }, + { + "type": "integer", + "description": "Container VMID (optional - if not provided, streams node journalctl)", + "name": "vmid", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Journalctl output", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "403": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "404": { + "description": "Node not found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "x-id": "journalctl", + "operationId": "journalctl" + } + }, "/proxmox/journalctl/{node}": { "get": { "description": "Get journalctl output for node or LXC container. If vmid is not provided, streams node journalctl.", @@ -2092,18 +2172,40 @@ ], "summary": "Get journalctl output", "parameters": [ + { + "maximum": 1000, + "minimum": 1, + "type": "integer", + "default": 100, + "description": "Limit output lines (1-1000)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)", + "name": "service", + "in": "query" + }, + { + "type": "integer", + "description": "Container VMID (optional - if not provided, streams node journalctl)", + "name": "vmid", + "in": "query" + }, { "type": "string", "description": "Node name", "name": "node", "in": "path", "required": true - }, - { - "type": "integer", - "description": "Limit output lines (1-1000)", - "name": "limit", - "in": "query" } ], "responses": { @@ -2157,6 +2259,34 @@ ], "summary": "Get journalctl output", "parameters": [ + { + "maximum": 1000, + "minimum": 1, + "type": "integer", + "default": 100, + "description": "Limit output lines (1-1000)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)", + "name": "service", + "in": "query" + }, + { + "type": "integer", + "description": "Container VMID (optional - if not provided, streams node journalctl)", + "name": "vmid", + "in": "query" + }, { "type": "string", "description": "Node name", @@ -2169,12 +2299,6 @@ "description": "Container VMID (optional - if not provided, streams node journalctl)", "name": "vmid", "in": "path" - }, - { - "type": "integer", - "description": "Limit output lines (1-1000)", - "name": "limit", - "in": "query" } ], "responses": { @@ -2228,18 +2352,40 @@ ], "summary": "Get journalctl output", "parameters": [ + { + "maximum": 1000, + "minimum": 1, + "type": "integer", + "default": 100, + "description": "Limit output lines (1-1000)", + "name": "limit", + "in": "query" + }, { "type": "string", "description": "Node name", "name": "node", - "in": "path", + "in": "query", "required": true }, + { + "type": "string", + "description": "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)", + "name": "service", + "in": "query" + }, { "type": "integer", "description": "Container VMID (optional - if not provided, streams node journalctl)", "name": "vmid", - "in": "path" + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "path", + "required": true }, { "type": "string", @@ -2249,9 +2395,9 @@ }, { "type": "integer", - "description": "Limit output lines (1-1000)", - "name": "limit", - "in": "query" + "description": "Container VMID (optional - if not provided, streams node journalctl)", + "name": "vmid", + "in": "path" } ], "responses": { diff --git a/internal/api/v1/docs/swagger.yaml b/internal/api/v1/docs/swagger.yaml index b8730fbd..8de1bdb8 100644 --- a/internal/api/v1/docs/swagger.yaml +++ b/internal/api/v1/docs/swagger.yaml @@ -3402,6 +3402,62 @@ paths: - metrics - websocket x-id: uptime + /proxmox/journalctl: + get: + consumes: + - application/json + description: Get journalctl output for node or LXC container. If vmid is not + provided, streams node journalctl. + parameters: + - default: 100 + description: Limit output lines (1-1000) + in: query + maximum: 1000 + minimum: 1 + name: limit + type: integer + - description: Node name + in: query + name: node + required: true + type: string + - description: Service name (e.g., 'pveproxy' for node, 'container@.service' + format for LXC) + in: query + name: service + type: string + - description: Container VMID (optional - if not provided, streams node journalctl) + in: query + name: vmid + type: integer + produces: + - application/json + responses: + "200": + description: Journalctl output + schema: + type: string + "400": + description: Invalid request + schema: + $ref: '#/definitions/ErrorResponse' + "403": + description: Unauthorized + schema: + $ref: '#/definitions/ErrorResponse' + "404": + description: Node not found + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Get journalctl output + tags: + - proxmox + - websocket + x-id: journalctl /proxmox/journalctl/{node}: get: consumes: @@ -3409,15 +3465,32 @@ paths: description: Get journalctl output for node or LXC container. If vmid is not provided, streams node journalctl. parameters: + - default: 100 + description: Limit output lines (1-1000) + in: query + maximum: 1000 + minimum: 1 + name: limit + type: integer + - description: Node name + in: query + name: node + required: true + type: string + - description: Service name (e.g., 'pveproxy' for node, 'container@.service' + format for LXC) + in: query + name: service + type: string + - description: Container VMID (optional - if not provided, streams node journalctl) + in: query + name: vmid + type: integer - description: Node name in: path name: node required: true type: string - - description: Limit output lines (1-1000) - in: query - name: limit - type: integer produces: - application/json responses: @@ -3453,6 +3526,27 @@ paths: description: Get journalctl output for node or LXC container. If vmid is not provided, streams node journalctl. parameters: + - default: 100 + description: Limit output lines (1-1000) + in: query + maximum: 1000 + minimum: 1 + name: limit + type: integer + - description: Node name + in: query + name: node + required: true + type: string + - description: Service name (e.g., 'pveproxy' for node, 'container@.service' + format for LXC) + in: query + name: service + type: string + - description: Container VMID (optional - if not provided, streams node journalctl) + in: query + name: vmid + type: integer - description: Node name in: path name: node @@ -3462,10 +3556,6 @@ paths: in: path name: vmid type: integer - - description: Limit output lines (1-1000) - in: query - name: limit - type: integer produces: - application/json responses: @@ -3501,23 +3591,40 @@ paths: description: Get journalctl output for node or LXC container. If vmid is not provided, streams node journalctl. parameters: + - default: 100 + description: Limit output lines (1-1000) + in: query + maximum: 1000 + minimum: 1 + name: limit + type: integer + - description: Node name + in: query + name: node + required: true + type: string + - description: Service name (e.g., 'pveproxy' for node, 'container@.service' + format for LXC) + in: query + name: service + type: string + - description: Container VMID (optional - if not provided, streams node journalctl) + in: query + name: vmid + type: integer - description: Node name in: path name: node required: true type: string - - description: Container VMID (optional - if not provided, streams node journalctl) - in: path - name: vmid - type: integer - description: Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC) in: path name: service type: string - - description: Limit output lines (1-1000) - in: query - name: limit + - description: Container VMID (optional - if not provided, streams node journalctl) + in: path + name: vmid type: integer produces: - application/json diff --git a/internal/api/v1/proxmox/journalctl.go b/internal/api/v1/proxmox/journalctl.go index 99cf9632..decfa2b6 100644 --- a/internal/api/v1/proxmox/journalctl.go +++ b/internal/api/v1/proxmox/journalctl.go @@ -1,6 +1,7 @@ package proxmoxapi import ( + "errors" "io" "net/http" @@ -11,10 +12,10 @@ import ( ) type JournalctlRequest struct { - Node string `uri:"node" binding:"required"` // Node name - VMID *int `uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl) - Service string `uri:"service"` // Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC) - Limit int `query:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000) + Node string `form:"node" uri:"node" binding:"required"` // Node name + VMID *int `form:"vmid" uri:"vmid"` // Container VMID (optional - if not provided, streams node journalctl) + Service string `form:"service" uri:"service"` // Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC) + Limit *int `form:"limit" uri:"limit" default:"100" binding:"min=1,max=1000"` // Limit output lines (1-1000) } // @name ProxmoxJournalctlRequest // @x-id "journalctl" @@ -24,26 +25,23 @@ type JournalctlRequest struct { // @Tags proxmox,websocket // @Accept json // @Produce application/json -// @Param node path string true "Node name" -// @Param vmid path int false "Container VMID (optional - if not provided, streams node journalctl)" -// @Param service path string false "Service name (e.g., 'pveproxy' for node, 'container@.service' format for LXC)" -// @Param limit query int false "Limit output lines (1-1000)" +// @Param query query JournalctlRequest true "Request" +// @Param path path JournalctlRequest true "Request" // @Success 200 string plain "Journalctl output" // @Failure 400 {object} apitypes.ErrorResponse "Invalid request" // @Failure 403 {object} apitypes.ErrorResponse "Unauthorized" // @Failure 404 {object} apitypes.ErrorResponse "Node not found" // @Failure 500 {object} apitypes.ErrorResponse "Internal server error" +// @Router /proxmox/journalctl [get] // @Router /proxmox/journalctl/{node} [get] // @Router /proxmox/journalctl/{node}/{vmid} [get] // @Router /proxmox/journalctl/{node}/{vmid}/{service} [get] func Journalctl(c *gin.Context) { var request JournalctlRequest - if err := c.ShouldBindUri(&request); err != nil { - c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) - return - } - if err := c.ShouldBindQuery(&request); err != nil { - c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) + uriErr := c.ShouldBindUri(&request) + queryErr := c.ShouldBindQuery(&request) + if uriErr != nil && queryErr != nil { // allow both uri and query parameters to be set + c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", errors.Join(uriErr, queryErr))) return } @@ -58,9 +56,9 @@ func Journalctl(c *gin.Context) { var reader io.ReadCloser var err error if request.VMID == nil { - reader, err = node.NodeJournalctl(c.Request.Context(), request.Service, request.Limit) + reader, err = node.NodeJournalctl(c.Request.Context(), request.Service, *request.Limit) } else { - reader, err = node.LXCJournalctl(c.Request.Context(), *request.VMID, request.Service, request.Limit) + reader, err = node.LXCJournalctl(c.Request.Context(), *request.VMID, request.Service, *request.Limit) } if err != nil { c.Error(apitypes.InternalServerError(err, "failed to get journalctl output"))