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(