diff --git a/backend/docs/swagger/docs.go b/backend/docs/swagger/docs.go index 6ab3aafd..26f58921 100644 --- a/backend/docs/swagger/docs.go +++ b/backend/docs/swagger/docs.go @@ -3322,6 +3322,62 @@ const docTemplate = `{ } } } + }, + "/workflows/retry/{workflow_uuid}": { + "post": { + "security": [ + { + "AdminMiddleware": [] + } + ], + "description": "Retries a workflow that is in failed state", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Retry failed workflow", + "operationId": "retry-failed-workflow", + "parameters": [ + { + "type": "string", + "description": "Workflow UUID", + "name": "workflow_uuid", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "Workflow retry initiated", + "schema": { + "$ref": "#/definitions/handlers.APIResponse" + } + }, + "400": { + "description": "Invalid workflow UUID or workflow not failed", + "schema": { + "$ref": "#/definitions/handlers.APIResponse" + } + }, + "404": { + "description": "Workflow not found", + "schema": { + "$ref": "#/definitions/handlers.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.APIResponse" + } + } + } + } } }, "definitions": { @@ -4153,6 +4209,9 @@ const docTemplate = `{ "type": "string" } }, + "healthy": { + "type": "boolean" + }, "ip": { "description": "Computed", "type": "string" @@ -4371,6 +4430,12 @@ const docTemplate = `{ "redeemed": { "type": "boolean" }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + }, "value": { "type": "number" } diff --git a/backend/docs/swagger/swagger.json b/backend/docs/swagger/swagger.json index 485993b7..79986137 100644 --- a/backend/docs/swagger/swagger.json +++ b/backend/docs/swagger/swagger.json @@ -3315,6 +3315,62 @@ } } } + }, + "/workflows/retry/{workflow_uuid}": { + "post": { + "security": [ + { + "AdminMiddleware": [] + } + ], + "description": "Retries a workflow that is in failed state", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Retry failed workflow", + "operationId": "retry-failed-workflow", + "parameters": [ + { + "type": "string", + "description": "Workflow UUID", + "name": "workflow_uuid", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "Workflow retry initiated", + "schema": { + "$ref": "#/definitions/handlers.APIResponse" + } + }, + "400": { + "description": "Invalid workflow UUID or workflow not failed", + "schema": { + "$ref": "#/definitions/handlers.APIResponse" + } + }, + "404": { + "description": "Workflow not found", + "schema": { + "$ref": "#/definitions/handlers.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.APIResponse" + } + } + } + } } }, "definitions": { @@ -4146,6 +4202,9 @@ "type": "string" } }, + "healthy": { + "type": "boolean" + }, "ip": { "description": "Computed", "type": "string" @@ -4364,6 +4423,12 @@ "redeemed": { "type": "boolean" }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + }, "value": { "type": "number" } diff --git a/backend/docs/swagger/swagger.yaml b/backend/docs/swagger/swagger.yaml index 883907d1..304517bb 100644 --- a/backend/docs/swagger/swagger.yaml +++ b/backend/docs/swagger/swagger.yaml @@ -550,6 +550,8 @@ definitions: items: type: string type: array + healthy: + type: boolean ip: description: Computed type: string @@ -709,6 +711,10 @@ definitions: type: integer redeemed: type: boolean + user_id: + type: integer + username: + type: string value: type: number required: @@ -3234,6 +3240,42 @@ paths: summary: List all workflows tags: - admin + /workflows/retry/{workflow_uuid}: + post: + consumes: + - application/json + description: Retries a workflow that is in failed state + operationId: retry-failed-workflow + parameters: + - description: Workflow UUID + in: path + name: workflow_uuid + required: true + type: string + produces: + - application/json + responses: + "202": + description: Workflow retry initiated + schema: + $ref: '#/definitions/handlers.APIResponse' + "400": + description: Invalid workflow UUID or workflow not failed + schema: + $ref: '#/definitions/handlers.APIResponse' + "404": + description: Workflow not found + schema: + $ref: '#/definitions/handlers.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.APIResponse' + security: + - AdminMiddleware: [] + summary: Retry failed workflow + tags: + - admin securityDefinitions: BearerAuth: in: header diff --git a/backend/internal/api/app/app.go b/backend/internal/api/app/app.go index fcd0cd61..85277a61 100644 --- a/backend/internal/api/app/app.go +++ b/backend/internal/api/app/app.go @@ -132,6 +132,7 @@ func (app *App) registerHandlers() { adminGroup.GET("/pending-records", app.handlers.adminHandler.ListPendingRecordsHandler) adminGroup.GET("/invoices", app.handlers.invoiceHandler.ListAllInvoicesHandler) adminGroup.GET("/workflows", app.handlers.adminHandler.ListAllWorkflowsHandler) + adminGroup.GET("/workflows/retry/:workflow_uuid", app.handlers.adminHandler.RetryFailedWorkflowHandler) vouchersGroup := adminGroup.Group("/vouchers") { diff --git a/backend/internal/api/handlers/admin_handler.go b/backend/internal/api/handlers/admin_handler.go index 2a138040..4ffcea27 100644 --- a/backend/internal/api/handlers/admin_handler.go +++ b/backend/internal/api/handlers/admin_handler.go @@ -532,3 +532,49 @@ func (h *AdminHandler) ListAllWorkflowsHandler(c *gin.Context) { "total_pages": (total + limit - 1) / limit, }) } + +// @Summary Retry failed workflow +// @Description Retries a workflow that is in failed state +// @Tags admin +// @ID retry-failed-workflow +// @Accept json +// @Produce json +// @Param workflow_uuid path string true "Workflow UUID" +// @Success 202 {object} APIResponse "Workflow retry initiated" +// @Failure 400 {object} APIResponse "Invalid workflow UUID or workflow not failed" +// @Failure 404 {object} APIResponse "Workflow not found" +// @Failure 500 {object} APIResponse +// @Security AdminMiddleware +// @Router /workflows/retry/{workflow_uuid} [post] +func (h *AdminHandler) RetryFailedWorkflowHandler(c *gin.Context) { + reqLog := requestLogger(c, "RetryFailedWorkflowHandler") + + workflowUUID := c.Param("workflow_uuid") + if strings.TrimSpace(workflowUUID) == "" { + BadRequest(c, "Workflow UUID is required") + return + } + + err := h.svc.RetryFailedWorkflow(workflowUUID) + if err != nil { + reqLog.Error(). + Err(err). + Str("workflow_uuid", workflowUUID). + Msg("failed to retry workflow") + + if errors.Is(err, services.ErrWorkflowNotFound) { + NotFound(c, "Workflow not found") + return + } + if errors.Is(err, services.ErrWorkflowNotFailed) { + BadRequest(c, "Only failed workflows can be retried") + return + } + InternalServerError(c) + return + } + + Accepted(c, "Workflow retry initiated", gin.H{ + "workflow_uuid": workflowUUID, + }) +} diff --git a/backend/internal/core/services/admin_service.go b/backend/internal/core/services/admin_service.go index 52fa8c9b..ca2fe1f7 100644 --- a/backend/internal/core/services/admin_service.go +++ b/backend/internal/core/services/admin_service.go @@ -20,6 +20,9 @@ import ( "github.com/xmonader/ewf" ) +var ErrWorkflowNotFound = fmt.Errorf("workflow not found") +var ErrWorkflowNotFailed = fmt.Errorf("workflow is not in failed state") + type AdminService struct { userRepo models.UserRepository nodesRepo models.UserNodesRepository @@ -441,3 +444,26 @@ func (svc *AdminService) sendBulkSystemMails(users []models.User, body string, s return len(failedEmails) } + +// RetryFailedWorkflow retries a failed workflow +func (svc *AdminService) RetryFailedWorkflow(workflowUUID string) error { + wf, err := svc.ewfRepo.LoadWorkflowByUUID(svc.appCtx, workflowUUID) + if err != nil { + logger.GetLogger().Error().Err(err). + Str("workflow_uuid", workflowUUID). + Msg("failed to get workflow for retry") + + return ErrWorkflowNotFound + } + + if wf.Status != ewf.StatusFailed { + return ErrWorkflowNotFailed + } + + // Reset workflow fields + wf.Status = ewf.StatusPending + wf.CurrentStep = 0 + wf.Error = "" + + return svc.ewfEngine.Run(svc.appCtx, wf, ewf.WithAsync()) +} diff --git a/frontend/kubecloud/src/components/AdminWorkflowsTable.vue b/frontend/kubecloud/src/components/AdminWorkflowsTable.vue index 92e9f10a..88d2e2f9 100644 --- a/frontend/kubecloud/src/components/AdminWorkflowsTable.vue +++ b/frontend/kubecloud/src/components/AdminWorkflowsTable.vue @@ -147,6 +147,18 @@ + + + Retry + Close @@ -186,6 +198,7 @@ const headers = [ const showWorkflowModal = ref(false) const selectedWorkflow = ref(null) +const retryLoading = ref(false) function getStatusColor(status: string): string { switch (status) { @@ -256,6 +269,20 @@ async function refreshWorkflows() { await loadWorkflows() } +async function retryWorkflowAction() { + if (!selectedWorkflow.value) return + retryLoading.value = true + try { + await adminService.retryWorkflow(selectedWorkflow.value.uuid) + closeWorkflowModal() + await loadWorkflows() + } catch (err) { + console.error('Failed to retry workflow:', err) + } finally { + retryLoading.value = false + } +} + onMounted(async () => { await loadWorkflows() }) diff --git a/frontend/kubecloud/src/utils/adminService.ts b/frontend/kubecloud/src/utils/adminService.ts index d44ef1ba..b1ede8cb 100644 --- a/frontend/kubecloud/src/utils/adminService.ts +++ b/frontend/kubecloud/src/utils/adminService.ts @@ -288,6 +288,17 @@ export class AdminService { } } + // Retry a failed workflow (admin) + async retryWorkflow(workflowUUID: string): Promise { + await api.get(`/v1/workflows/retry/${encodeURIComponent(workflowUUID)}`, { + requiresAuth: true, + showNotifications: true, + loadingMessage: 'Retrying workflow...', + successMessage: 'Workflow retry initiated', + errorMessage: 'Failed to retry workflow', + }) + } + async SetMaintenanceModeStatus(status: boolean): Promise { try { const response = await api.put(