mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-09-28 04:59:17 +00:00
Show human readable information in queue info (#5516)
Co-authored-by: qwerty287 <80460567+qwerty287@users.noreply.github.com>
This commit is contained in:
@@ -1777,7 +1777,7 @@ const docTemplate = `{
|
||||
},
|
||||
"/queue/info": {
|
||||
"get": {
|
||||
"description": "TODO: link the InfoT response object - this is blocked, until the ` + "`" + `swaggo/swag` + "`" + ` tool dependency is v1.18.12 or newer",
|
||||
"description": "Returns pipeline queue information with agent details",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -1799,10 +1799,7 @@ const docTemplate = `{
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
"$ref": "#/definitions/QueueInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5019,6 +5016,49 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"QueueInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"paused": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"pending": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.QueueTask"
|
||||
}
|
||||
},
|
||||
"running": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.QueueTask"
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pending_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"running_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"waiting_on_deps_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"worker_count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"waiting_on_deps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.QueueTask"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Registry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -5453,6 +5493,18 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pid": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pipeline_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"repo_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"run_on": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -5803,6 +5855,59 @@ const docTemplate = `{
|
||||
"ForgeTypeAddon"
|
||||
]
|
||||
},
|
||||
"model.QueueTask": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"agent_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"dep_status": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/StatusValue"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pid": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pipeline_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pipeline_number": {
|
||||
"type": "integer"
|
||||
},
|
||||
"repo_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"run_on": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.TrustedConfiguration": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@@ -2,7 +2,8 @@
|
||||
FROM docker.io/golang:1.25-alpine AS golang_image
|
||||
FROM docker.io/node:23-alpine
|
||||
|
||||
RUN apk add --no-cache --update make gcc binutils-gold musl-dev protoc && \
|
||||
RUN apk add --no-cache --update make gcc binutils-gold musl-dev && \
|
||||
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main protoc && \
|
||||
corepack enable
|
||||
|
||||
# Build packages.
|
||||
|
@@ -16,4 +16,4 @@ package proto
|
||||
|
||||
// Version is the version of the woodpecker.proto file,
|
||||
// IMPORTANT: increased by 1 each time it get changed.
|
||||
const Version int32 = 13
|
||||
const Version int32 = 14
|
||||
|
@@ -15,7 +15,7 @@
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc-gen-go v1.36.9
|
||||
// protoc v6.31.1
|
||||
// source: woodpecker.proto
|
||||
|
||||
|
@@ -37,16 +37,115 @@ import (
|
||||
// GetQueueInfo
|
||||
//
|
||||
// @Summary Get pipeline queue information
|
||||
// @Description TODO: link the InfoT response object - this is blocked, until the `swaggo/swag` tool dependency is v1.18.12 or newer
|
||||
// @Description Returns pipeline queue information with agent details
|
||||
// @Router /queue/info [get]
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Success 200 {object} QueueInfo
|
||||
// @Tags Pipeline queues
|
||||
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
|
||||
func GetQueueInfo(c *gin.Context) {
|
||||
c.IndentedJSON(http.StatusOK,
|
||||
server.Config.Services.Queue.Info(c),
|
||||
)
|
||||
info := server.Config.Services.Queue.Info(c)
|
||||
_store := store.FromContext(c)
|
||||
|
||||
// Create a map to store agent names by ID
|
||||
agentNameMap := make(map[int64]string)
|
||||
|
||||
// Process tasks and add agent names
|
||||
pendingWithAgents, err := processQueueTasks(_store, info.Pending, agentNameMap)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
waitingWithAgents, err := processQueueTasks(_store, info.WaitingOnDeps, agentNameMap)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
runningWithAgents, err := processQueueTasks(_store, info.Running, agentNameMap)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Create response with agent-enhanced tasks
|
||||
response := model.QueueInfo{
|
||||
Pending: pendingWithAgents,
|
||||
WaitingOnDeps: waitingWithAgents,
|
||||
Running: runningWithAgents,
|
||||
Stats: struct {
|
||||
WorkerCount int `json:"worker_count"`
|
||||
PendingCount int `json:"pending_count"`
|
||||
WaitingOnDepsCount int `json:"waiting_on_deps_count"`
|
||||
RunningCount int `json:"running_count"`
|
||||
}{
|
||||
WorkerCount: info.Stats.Workers,
|
||||
PendingCount: info.Stats.Pending,
|
||||
WaitingOnDepsCount: info.Stats.WaitingOnDeps,
|
||||
RunningCount: info.Stats.Running,
|
||||
},
|
||||
Paused: info.Paused,
|
||||
}
|
||||
|
||||
c.IndentedJSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// getAgentName finds an agent's name, utilizing a map as a cache.
|
||||
func getAgentName(store store.Store, agentNameMap map[int64]string, agentID int64) (string, bool) {
|
||||
// 1. Check the cache first.
|
||||
name, exists := agentNameMap[agentID]
|
||||
if exists {
|
||||
return name, true
|
||||
}
|
||||
|
||||
// 2. If not in cache, query the store.
|
||||
agent, err := store.AgentFind(agentID)
|
||||
if err != nil || agent == nil {
|
||||
// Agent not found or an error occurred.
|
||||
return "", false
|
||||
}
|
||||
|
||||
// 3. Found the agent, update the cache and return the name.
|
||||
if agent.Name != "" {
|
||||
agentNameMap[agentID] = agent.Name
|
||||
return agent.Name, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// processQueueTasks converts tasks to QueueTask structs and adds agent names.
|
||||
func processQueueTasks(store store.Store, tasks []*model.Task, agentNameMap map[int64]string) ([]model.QueueTask, error) {
|
||||
result := make([]model.QueueTask, 0, len(tasks))
|
||||
|
||||
for _, task := range tasks {
|
||||
taskResponse := model.QueueTask{
|
||||
Task: *task,
|
||||
}
|
||||
|
||||
if task.AgentID == 0 {
|
||||
result = append(result, taskResponse)
|
||||
continue
|
||||
}
|
||||
|
||||
name, ok := getAgentName(store, agentNameMap, task.AgentID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("agent not found for task %s", task.ID)
|
||||
}
|
||||
|
||||
taskResponse.AgentName = name
|
||||
|
||||
p, err := store.GetPipeline(task.PipelineID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskResponse.PipelineNumber = p.Number
|
||||
|
||||
result = append(result, taskResponse)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PauseQueue
|
||||
|
@@ -15,6 +15,7 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
pipelineConsts "go.woodpecker-ci.org/woodpecker/v3/pipeline"
|
||||
@@ -25,15 +26,18 @@ import (
|
||||
|
||||
func createFilterFunc(agentFilter rpc.Filter) queue.FilterFn {
|
||||
return func(task *model.Task) (bool, int) {
|
||||
// Create a copy of the labels for filtering to avoid modifying the original task
|
||||
labels := maps.Clone(task.Labels)
|
||||
|
||||
// ignore internal labels for filtering
|
||||
for k := range task.Labels {
|
||||
for k := range labels {
|
||||
if strings.HasPrefix(k, pipelineConsts.InternalLabelPrefix) {
|
||||
delete(task.Labels, k)
|
||||
delete(labels, k)
|
||||
}
|
||||
}
|
||||
|
||||
score := 0
|
||||
for taskLabel, taskLabelValue := range task.Labels {
|
||||
for taskLabel, taskLabelValue := range labels {
|
||||
// if a task label is empty it will be ignored
|
||||
if taskLabelValue == "" {
|
||||
continue
|
||||
|
22
server/model/queue.go
Normal file
22
server/model/queue.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package model
|
||||
|
||||
// QueueTask represents a task in the queue with additional API-specific fields.
|
||||
type QueueTask struct {
|
||||
Task
|
||||
PipelineNumber int64 `json:"pipeline_number"`
|
||||
AgentName string `json:"agent_name"`
|
||||
}
|
||||
|
||||
// QueueInfo represents the response structure for queue information API.
|
||||
type QueueInfo struct {
|
||||
Pending []QueueTask `json:"pending"`
|
||||
WaitingOnDeps []QueueTask `json:"waiting_on_deps"`
|
||||
Running []QueueTask `json:"running"`
|
||||
Stats struct {
|
||||
WorkerCount int `json:"worker_count"`
|
||||
PendingCount int `json:"pending_count"`
|
||||
WaitingOnDepsCount int `json:"waiting_on_deps_count"`
|
||||
RunningCount int `json:"running_count"`
|
||||
} `json:"stats"`
|
||||
Paused bool `json:"paused"`
|
||||
} // @name QueueInfo
|
@@ -25,12 +25,16 @@ import (
|
||||
// Task defines scheduled pipeline Task.
|
||||
type Task struct {
|
||||
ID string `json:"id" xorm:"PK UNIQUE 'id'"`
|
||||
PID int `json:"pid" xorm:"'pid'"`
|
||||
Name string `json:"name" xorm:"'name'"`
|
||||
Data []byte `json:"-" xorm:"LONGBLOB 'data'"`
|
||||
Labels map[string]string `json:"labels" xorm:"json 'labels'"`
|
||||
Dependencies []string `json:"dependencies" xorm:"json 'dependencies'"`
|
||||
RunOn []string `json:"run_on" xorm:"json 'run_on'"`
|
||||
DepStatus map[string]StatusValue `json:"dep_status" xorm:"json 'dependencies_status'"`
|
||||
AgentID int64 `json:"agent_id" xorm:"'agent_id'"`
|
||||
PipelineID int64 `json:"pipeline_id" xorm:"'pipeline_id'"`
|
||||
RepoID int64 `json:"repo_id" xorm:"'repo_id'"`
|
||||
} // @name Task
|
||||
|
||||
// TableName return database table name for xorm.
|
||||
|
@@ -34,7 +34,11 @@ func queuePipeline(ctx context.Context, repo *model.Repo, pipelineItems []*stepb
|
||||
}
|
||||
task := &model.Task{
|
||||
ID: fmt.Sprint(item.Workflow.ID),
|
||||
PID: item.Workflow.PID,
|
||||
Name: item.Workflow.Name,
|
||||
Labels: make(map[string]string),
|
||||
PipelineID: item.Workflow.PipelineID,
|
||||
RepoID: repo.ID,
|
||||
}
|
||||
maps.Copy(task.Labels, item.Labels)
|
||||
err := task.ApplyLabelsFromRepo(repo)
|
||||
|
@@ -271,7 +271,8 @@
|
||||
"no_permission": "You are not allowed to access the debug information",
|
||||
"metadata_exec_title": "Re-run pipeline locally",
|
||||
"metadata_exec_desc": "Download the metadata of this pipeline to run it locally. This allows you to fix problems and test changes before committing them. The Woodpecker CLI must be installed locally in the same version as the server."
|
||||
}
|
||||
},
|
||||
"view": "View pipeline"
|
||||
}
|
||||
},
|
||||
"org": {
|
||||
@@ -341,7 +342,10 @@
|
||||
"badge": "org"
|
||||
},
|
||||
"version": "Version",
|
||||
"last_contact": {
|
||||
"last_contact": "Last contact",
|
||||
"badge": "last contact"
|
||||
},
|
||||
"never": "Never",
|
||||
"delete_confirm": "Do you really want to delete this agent? It will no longer be able to connect to the server.",
|
||||
"edit_agent": "Edit agent",
|
||||
|
@@ -51,7 +51,7 @@
|
||||
<TextField :id="id" :model-value="agent.version" disabled />
|
||||
</InputField>
|
||||
|
||||
<InputField v-slot="{ id }" :label="$t('admin.settings.agents.last_contact')">
|
||||
<InputField v-slot="{ id }" :label="$t('admin.settings.agents.last_contact.last_contact')">
|
||||
<TextField
|
||||
:id="id"
|
||||
:model-value="
|
||||
|
@@ -3,11 +3,10 @@
|
||||
<ListItem
|
||||
v-for="agent in agents"
|
||||
:key="agent.id"
|
||||
class="bg-wp-background-200! dark:bg-wp-background-100! items-center"
|
||||
class="bg-wp-background-200! dark:bg-wp-background-100! items-center gap-2"
|
||||
>
|
||||
<span>{{ agent.name || `Agent ${agent.id}` }}</span>
|
||||
<span class="ml-auto">
|
||||
<span class="hidden space-x-2 md:inline-block">
|
||||
<span class="ml-auto flex gap-2">
|
||||
<Badge
|
||||
v-if="isAdmin === true && agent.org_id !== -1"
|
||||
:label="$t('admin.settings.agents.org.badge')"
|
||||
@@ -16,12 +15,12 @@
|
||||
<Badge v-if="agent.platform" :label="$t('admin.settings.agents.platform.badge')" :value="agent.platform" />
|
||||
<Badge v-if="agent.backend" :label="$t('admin.settings.agents.backend.badge')" :value="agent.backend" />
|
||||
<Badge v-if="agent.capacity" :label="$t('admin.settings.agents.capacity.badge')" :value="agent.capacity" />
|
||||
<Badge
|
||||
:label="$t('admin.settings.agents.last_contact.badge')"
|
||||
:value="agent.last_contact ? date.timeAgo(agent.last_contact * 1000) : $t('admin.settings.agents.never')"
|
||||
/>
|
||||
</span>
|
||||
<span class="ml-2">{{
|
||||
agent.last_contact ? date.timeAgo(agent.last_contact * 1000) : $t('admin.settings.agents.never')
|
||||
}}</span>
|
||||
</span>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<div class="ml-2 flex items-center gap-2">
|
||||
<IconButton
|
||||
icon="edit"
|
||||
:title="$t('admin.settings.agents.edit_agent')"
|
||||
|
@@ -1,10 +1,16 @@
|
||||
export interface Task {
|
||||
id: number;
|
||||
pid: number;
|
||||
name: string;
|
||||
labels: Record<string, string>;
|
||||
dependencies: string[];
|
||||
dep_status: Record<string, string>;
|
||||
run_on: string[];
|
||||
agent_id: number;
|
||||
agent_name: string;
|
||||
pipeline_id: number;
|
||||
pipeline_number: number;
|
||||
repo_id: number;
|
||||
}
|
||||
|
||||
export interface QueueStats {
|
||||
|
@@ -32,7 +32,7 @@
|
||||
:key="task.id"
|
||||
class="bg-wp-background-200! dark:bg-wp-background-200! mb-2 flex-col items-center gap-4"
|
||||
>
|
||||
<div class="flex w-full items-center justify-between border-b pb-2">
|
||||
<div class="flex w-full items-center justify-between gap-2 border-b pb-2">
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
:title="
|
||||
@@ -57,11 +57,11 @@
|
||||
'text-wp-state-neutral-100': task.status === 'pending',
|
||||
}"
|
||||
/>
|
||||
<span>{{ task.id }}</span>
|
||||
<span>{{ task.name }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="ml-auto flex gap-2">
|
||||
<Badge v-if="task.agent_id !== 0" :label="$t('admin.settings.queue.agent')" :value="task.agent_id" />
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<span class="flex gap-2">
|
||||
<Badge v-if="task.agent_name" :label="$t('admin.settings.queue.agent')" :value="task.agent_name" />
|
||||
<Badge
|
||||
v-if="task.dependencies"
|
||||
:label="$t('admin.settings.queue.waiting_for')"
|
||||
@@ -69,6 +69,17 @@
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="ml-2 flex items-center gap-2">
|
||||
<IconButton
|
||||
icon="chevron-right"
|
||||
:title="$t('repo.pipeline.view')"
|
||||
class="h-8 w-8"
|
||||
:to="{
|
||||
name: 'repo-pipeline',
|
||||
params: { repoId: task.repo_id, pipelineId: task.pipeline_number, stepId: task.pid },
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-wrap gap-2">
|
||||
<template v-for="(value, label) in task.labels">
|
||||
@@ -89,6 +100,7 @@ import AdminQueueStats from '~/components/admin/settings/queue/AdminQueueStats.v
|
||||
import Badge from '~/components/atomic/Badge.vue';
|
||||
import Button from '~/components/atomic/Button.vue';
|
||||
import Icon from '~/components/atomic/Icon.vue';
|
||||
import IconButton from '~/components/atomic/IconButton.vue';
|
||||
import ListItem from '~/components/atomic/ListItem.vue';
|
||||
import Settings from '~/components/layout/Settings.vue';
|
||||
import useApiClient from '~/compositions/useApiClient';
|
||||
|
Reference in New Issue
Block a user