Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions backend/docs/swagger/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3322,6 +3322,62 @@ const docTemplate = `{
}
}
}
},
"/workflows/{workflow_uuid}/retry": {
"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": {
Expand Down Expand Up @@ -4153,6 +4209,9 @@ const docTemplate = `{
"type": "string"
}
},
"healthy": {
"type": "boolean"
},
"ip": {
"description": "Computed",
"type": "string"
Expand Down Expand Up @@ -4371,6 +4430,12 @@ const docTemplate = `{
"redeemed": {
"type": "boolean"
},
"user_id": {
"type": "integer"
},
"username": {
"type": "string"
},
"value": {
"type": "number"
}
Expand Down
65 changes: 65 additions & 0 deletions backend/docs/swagger/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -3315,6 +3315,62 @@
}
}
}
},
"/workflows/{workflow_uuid}/retry": {
"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": {
Expand Down Expand Up @@ -4146,6 +4202,9 @@
"type": "string"
}
},
"healthy": {
"type": "boolean"
},
"ip": {
"description": "Computed",
"type": "string"
Expand Down Expand Up @@ -4364,6 +4423,12 @@
"redeemed": {
"type": "boolean"
},
"user_id": {
"type": "integer"
},
"username": {
"type": "string"
},
"value": {
"type": "number"
}
Expand Down
42 changes: 42 additions & 0 deletions backend/docs/swagger/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,8 @@ definitions:
items:
type: string
type: array
healthy:
type: boolean
ip:
description: Computed
type: string
Expand Down Expand Up @@ -709,6 +711,10 @@ definitions:
type: integer
redeemed:
type: boolean
user_id:
type: integer
username:
type: string
value:
type: number
required:
Expand Down Expand Up @@ -3234,6 +3240,42 @@ paths:
summary: List all workflows
tags:
- admin
/workflows/{workflow_uuid}/retry:
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
Expand Down
1 change: 1 addition & 0 deletions backend/internal/api/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
{
Expand Down
46 changes: 46 additions & 0 deletions backend/internal/api/handlers/admin_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/{workflow_uuid}/retry [post]
Comment thread
RawanMostafa08 marked this conversation as resolved.
Outdated
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,
})
}
22 changes: 22 additions & 0 deletions backend/internal/core/services/admin_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -441,3 +444,22 @@ 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 {
return ErrWorkflowNotFound
Comment thread
RawanMostafa08 marked this conversation as resolved.
}

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())
}
27 changes: 27 additions & 0 deletions frontend/kubecloud/src/components/AdminWorkflowsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@
</div>
</v-card-text>
<v-card-actions class="justify-end mt-2">
<v-btn
v-if="selectedWorkflow && selectedWorkflow.status === 'failed'"
size="small"
variant="outlined"
color="primary"
class="mr-2"
:loading="retryLoading"
@click="retryWorkflowAction"
>
<v-icon icon="mdi-replay" size="16" class="mr-1"></v-icon>
Retry
</v-btn>
<v-btn text color="grey-lighten-1" @click="closeWorkflowModal">Close</v-btn>
</v-card-actions>
</v-card>
Expand Down Expand Up @@ -186,6 +198,7 @@ const headers = [

const showWorkflowModal = ref(false)
const selectedWorkflow = ref<AdminWorkflow | null>(null)
const retryLoading = ref(false)

function getStatusColor(status: string): string {
switch (status) {
Expand Down Expand Up @@ -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()
})
Expand Down
11 changes: 11 additions & 0 deletions frontend/kubecloud/src/utils/adminService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,17 @@ export class AdminService {
}
}

// Retry a failed workflow (admin)
async retryWorkflow(workflowUUID: string): Promise<void> {
await api.get<void>(`/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<void> {
try {
const response = await api.put(
Expand Down