mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 12:50:05 +01:00
initial jobs dashboard
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
postgres
|
||||||
|
.idea
|
||||||
|
docker-compose.yml
|
||||||
|
atlas.hcl
|
||||||
|
autoglue
|
||||||
|
config.yaml
|
||||||
|
.env
|
||||||
101
.github/workflows/docker-publish.yml
vendored
Normal file
101
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
name: Docker Publish
|
||||||
|
|
||||||
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
|
# They are provided by a third-party and are governed by
|
||||||
|
# separate terms of service, privacy policy, and support
|
||||||
|
# documentation.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
# Publish semver tags as releases.
|
||||||
|
tags: [ 'v*.*.*' ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Use docker.io for Docker Hub if empty
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
# github.repository as <account>/<repo>
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
# This is used to complete the identity challenge
|
||||||
|
# with sigstore/fulcio when running outside of PRs.
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
|
||||||
|
# Install the cosign tool except on PR
|
||||||
|
# https://github.com/sigstore/cosign-installer
|
||||||
|
- name: Install cosign
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||||
|
with:
|
||||||
|
cosign-release: 'v2.2.4'
|
||||||
|
|
||||||
|
# Set up BuildKit Docker container builder to be able to build
|
||||||
|
# multi-platform images and export cache
|
||||||
|
# https://github.com/docker/setup-buildx-action
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
|
# Login against a Docker registry except on PR
|
||||||
|
# https://github.com/docker/login-action
|
||||||
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Extract metadata (tags, labels) for Docker
|
||||||
|
# https://github.com/docker/metadata-action
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
type=ref,event=branch
|
||||||
|
type=raw,value=latest
|
||||||
|
|
||||||
|
# Build and push Docker image with Buildx (don't push on PR)
|
||||||
|
# https://github.com/docker/build-push-action
|
||||||
|
- name: Build and push Docker image
|
||||||
|
id: build-and-push
|
||||||
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
# Sign the resulting Docker image digest except on PRs.
|
||||||
|
# This will only write to the public Rekor transparency log when the Docker
|
||||||
|
# repository is public to avoid leaking data. If you would like to publish
|
||||||
|
# transparency data even for private images, pass --force to cosign below.
|
||||||
|
# https://github.com/sigstore/cosign
|
||||||
|
- name: Sign the published Docker image
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
env:
|
||||||
|
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
|
||||||
|
TAGS: ${{ steps.meta.outputs.tags }}
|
||||||
|
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||||
|
# This step uses the identity token to provision an ephemeral certificate
|
||||||
|
# against the sigstore community Fulcio instance.
|
||||||
|
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
|
||||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#################################
|
||||||
|
# Builder: Go + Node in one
|
||||||
|
#################################
|
||||||
|
FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
git ca-certificates tzdata \
|
||||||
|
build-base \
|
||||||
|
nodejs npm
|
||||||
|
|
||||||
|
RUN npm i -g yarn pnpm
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN make swagger && make build
|
||||||
|
|
||||||
|
|
||||||
|
#################################
|
||||||
|
# Runtime
|
||||||
|
#################################
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata \
|
||||||
|
&& addgroup -S app && adduser -S app -G app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /src/autoglue /app/autoglue
|
||||||
|
|
||||||
|
ENV PORT=8080
|
||||||
|
EXPOSE 8080
|
||||||
|
USER app
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD wget -qO- "http://127.0.0.1:${PORT}/api/healthz" || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/autoglue"]
|
||||||
@@ -30,8 +30,9 @@ var serveCmd = &cobra.Command{
|
|||||||
Long: "Start the server",
|
Long: "Start the server",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
db.Connect()
|
db.Connect()
|
||||||
|
gdb := db.DB
|
||||||
|
|
||||||
jobs, err := bg.NewJobs()
|
jobs, err := bg.NewJobs(gdb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to init background jobs: %v", err)
|
log.Fatalf("failed to init background jobs: %v", err)
|
||||||
}
|
}
|
||||||
@@ -44,23 +45,27 @@ var serveCmd = &cobra.Command{
|
|||||||
}()
|
}()
|
||||||
defer jobs.Stop()
|
defer jobs.Stop()
|
||||||
|
|
||||||
// Enqueue one job immediately
|
{
|
||||||
/*
|
// schedule next 03:30 local time
|
||||||
id := uuid.NewString()
|
now := time.Now()
|
||||||
if _, err := jobs.Enqueue(context.Background(), id, "bootstrap_bastion", bg.BastionBootstrapArgs{}); err != nil {
|
next := time.Date(now.Year(), now.Month(), now.Day(), 3, 30, 0, 0, now.Location())
|
||||||
log.Fatalf("[enqueue] failed (job_id=%s): %v", id, err)
|
if !next.After(now) {
|
||||||
|
next = next.Add(24 * time.Hour)
|
||||||
}
|
}
|
||||||
log.Printf("[enqueue] queued (job_id=%s)", id)
|
|
||||||
|
|
||||||
// Verify the row exists
|
_, err := jobs.Enqueue(
|
||||||
if got, err := jobs.Client.Get(context.Background(), id); err != nil {
|
context.Background(),
|
||||||
log.Fatalf("[verify] Get failed (job_id=%s): %v", id, err)
|
uuid.NewString(),
|
||||||
} else if j, ok := got.(*job.Job); ok {
|
"archer_cleanup",
|
||||||
log.Printf("[verify] Get ok (job_id=%s, status=%s)", j.ID, j.Status)
|
bg.CleanupArgs{RetainDays: 7, Table: "jobs"},
|
||||||
} else {
|
archer.WithScheduleTime(next),
|
||||||
log.Printf("[verify] Get ok (job_id=%s) but unexpected type %T", id, got)
|
archer.WithMaxRetries(1),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to enqueue archer_cleanup: %v", err)
|
||||||
}
|
}
|
||||||
*/
|
}
|
||||||
|
|
||||||
// Periodic scheduler
|
// Periodic scheduler
|
||||||
schedCtx, schedCancel := context.WithCancel(context.Background())
|
schedCtx, schedCancel := context.WithCancel(context.Background())
|
||||||
defer schedCancel()
|
defer schedCancel()
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
services:
|
services:
|
||||||
# autoglue:
|
autoglue:
|
||||||
# image: ghcr.io/glueops/autoglue:latest
|
# image: ghcr.io/glueops/autoglue:latest
|
||||||
# build:
|
build: .
|
||||||
# context: .
|
ports:
|
||||||
# dockerfile: Dockerfile
|
- 8080:8080
|
||||||
# ports:
|
expose:
|
||||||
# - 8080:8080
|
- 8080
|
||||||
# expose:
|
env_file: .env
|
||||||
# - 8080
|
environment:
|
||||||
# env_file: .env
|
AUTOGLUE_DATABASE_DSN: postgres://$DB_USER:$DB_PASSWORD@postgres:5432/$DB_NAME
|
||||||
# environment:
|
AUTOGLUE_BIND_ADDRESS: 0.0.0.0
|
||||||
# AUTOGLUE_DATABASE_DSN: postgres://autoglue:autoglue@postgres:5432/autoglue
|
depends_on:
|
||||||
# AUTOGLUE_BIND_ADDRESS: 0.0.0.0
|
- postgres
|
||||||
# depends_on:
|
|
||||||
# - postgres
|
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
build:
|
build:
|
||||||
|
|||||||
458
docs/docs.go
458
docs/docs.go
@@ -2223,6 +2223,360 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/jobs/active": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Currently running jobs (limit default 100)",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Active jobs",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"default": 100,
|
||||||
|
"description": "Max rows",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/jobs.JobListItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/jobs/enqueue": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Schedules a job on a queue with optional args/schedule",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Manually enqueue a job",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Enqueue request",
|
||||||
|
"name": "payload",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/jobs.EnqueueReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"202": {
|
||||||
|
"description": "Accepted",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/jobs.EnqueueResp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "bad request",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/jobs/failures": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Failed jobs ordered by most recent (limit default 100)",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Recent failures",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"default": 100,
|
||||||
|
"description": "Max rows",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/jobs.JobListItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/jobs/kpi": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Aggregated counters across all queues",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Jobs KPI",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/jobs.KPI"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/jobs/queues": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Counts and avg duration per queue (last 24h)",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Per-queue rollups",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/jobs.QueueRollup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/jobs/{id}/cancel": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Cancels running or scheduled jobs",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Cancel a job",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Job ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "no content",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "bad request",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "not found",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/jobs/{id}/retry": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Calls Archer ScheduleNow on the job id",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Retry a job immediately",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Job ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "no content",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "bad request",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "not found",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/labels": {
|
"/api/v1/labels": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -6335,6 +6689,110 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jobs.EnqueueReq": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"jobs.EnqueueResp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jobs.JobListItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"last_error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"max_retry": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"queue_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"retry_count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"scheduled_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jobs.KPI": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dueNow": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"failed24h": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"retryable": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"runningNow": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"scheduledFuture": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"succeeded24h": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jobs.QueueRollup": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"avgDurationSecs": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float64"
|
||||||
|
},
|
||||||
|
"failed24h": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"queueName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"queuedDue": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"queuedFuture": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"running": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"success24h": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"labels.addLabelToPoolRequest": {
|
"labels.addLabelToPoolRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -2219,6 +2219,360 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/jobs/active": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Currently running jobs (limit default 100)",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Active jobs",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"default": 100,
|
||||||
|
"description": "Max rows",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/jobs.JobListItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/jobs/enqueue": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Schedules a job on a queue with optional args/schedule",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Manually enqueue a job",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Enqueue request",
|
||||||
|
"name": "payload",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/jobs.EnqueueReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"202": {
|
||||||
|
"description": "Accepted",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/jobs.EnqueueResp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "bad request",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/jobs/failures": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Failed jobs ordered by most recent (limit default 100)",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Recent failures",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"default": 100,
|
||||||
|
"description": "Max rows",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/jobs.JobListItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/jobs/kpi": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Aggregated counters across all queues",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Jobs KPI",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/jobs.KPI"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/jobs/queues": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Counts and avg duration per queue (last 24h)",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Per-queue rollups",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/jobs.QueueRollup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/jobs/{id}/cancel": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Cancels running or scheduled jobs",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Cancel a job",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Job ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "no content",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "bad request",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "not found",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/jobs/{id}/retry": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Calls Archer ScheduleNow on the job id",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"jobs"
|
||||||
|
],
|
||||||
|
"summary": "Retry a job immediately",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Job ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "no content",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "bad request",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "not found",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/labels": {
|
"/api/v1/labels": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -6331,6 +6685,110 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jobs.EnqueueReq": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"jobs.EnqueueResp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jobs.JobListItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"last_error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"max_retry": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"queue_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"retry_count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"scheduled_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"started_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jobs.KPI": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dueNow": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"failed24h": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"retryable": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"runningNow": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"scheduledFuture": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"succeeded24h": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jobs.QueueRollup": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"avgDurationSecs": {
|
||||||
|
"type": "number",
|
||||||
|
"format": "float64"
|
||||||
|
},
|
||||||
|
"failed24h": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"queueName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"queuedDue": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"queuedFuture": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"running": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"success24h": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"labels.addLabelToPoolRequest": {
|
"labels.addLabelToPoolRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -337,6 +337,78 @@ definitions:
|
|||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
jobs.EnqueueReq:
|
||||||
|
type: object
|
||||||
|
jobs.EnqueueResp:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
jobs.JobListItem:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
last_error:
|
||||||
|
type: string
|
||||||
|
max_retry:
|
||||||
|
type: integer
|
||||||
|
queue_name:
|
||||||
|
type: string
|
||||||
|
retry_count:
|
||||||
|
type: integer
|
||||||
|
scheduled_at:
|
||||||
|
type: string
|
||||||
|
started_at:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
jobs.KPI:
|
||||||
|
properties:
|
||||||
|
dueNow:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
failed24h:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
retryable:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
runningNow:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
scheduledFuture:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
succeeded24h:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
jobs.QueueRollup:
|
||||||
|
properties:
|
||||||
|
avgDurationSecs:
|
||||||
|
format: float64
|
||||||
|
type: number
|
||||||
|
failed24h:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
queueName:
|
||||||
|
type: string
|
||||||
|
queuedDue:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
queuedFuture:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
running:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
success24h:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
labels.addLabelToPoolRequest:
|
labels.addLabelToPoolRequest:
|
||||||
properties:
|
properties:
|
||||||
node_pool_ids:
|
node_pool_ids:
|
||||||
@@ -2191,6 +2263,230 @@ paths:
|
|||||||
summary: Detach one node pool from a cluster (org scoped)
|
summary: Detach one node pool from a cluster (org scoped)
|
||||||
tags:
|
tags:
|
||||||
- clusters
|
- clusters
|
||||||
|
/api/v1/jobs/{id}/cancel:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Cancels running or scheduled jobs
|
||||||
|
parameters:
|
||||||
|
- description: Job ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: no content
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"400":
|
||||||
|
description: bad request
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"500":
|
||||||
|
description: internal error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Cancel a job
|
||||||
|
tags:
|
||||||
|
- jobs
|
||||||
|
/api/v1/jobs/{id}/retry:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Calls Archer ScheduleNow on the job id
|
||||||
|
parameters:
|
||||||
|
- description: Job ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: no content
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"400":
|
||||||
|
description: bad request
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"500":
|
||||||
|
description: internal error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Retry a job immediately
|
||||||
|
tags:
|
||||||
|
- jobs
|
||||||
|
/api/v1/jobs/active:
|
||||||
|
get:
|
||||||
|
description: Currently running jobs (limit default 100)
|
||||||
|
parameters:
|
||||||
|
- default: 100
|
||||||
|
description: Max rows
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/jobs.JobListItem'
|
||||||
|
type: array
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"500":
|
||||||
|
description: internal error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Active jobs
|
||||||
|
tags:
|
||||||
|
- jobs
|
||||||
|
/api/v1/jobs/enqueue:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Schedules a job on a queue with optional args/schedule
|
||||||
|
parameters:
|
||||||
|
- description: Enqueue request
|
||||||
|
in: body
|
||||||
|
name: payload
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/jobs.EnqueueReq'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"202":
|
||||||
|
description: Accepted
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/jobs.EnqueueResp'
|
||||||
|
"400":
|
||||||
|
description: bad request
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"500":
|
||||||
|
description: internal error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Manually enqueue a job
|
||||||
|
tags:
|
||||||
|
- jobs
|
||||||
|
/api/v1/jobs/failures:
|
||||||
|
get:
|
||||||
|
description: Failed jobs ordered by most recent (limit default 100)
|
||||||
|
parameters:
|
||||||
|
- default: 100
|
||||||
|
description: Max rows
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/jobs.JobListItem'
|
||||||
|
type: array
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"500":
|
||||||
|
description: internal error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Recent failures
|
||||||
|
tags:
|
||||||
|
- jobs
|
||||||
|
/api/v1/jobs/kpi:
|
||||||
|
get:
|
||||||
|
description: Aggregated counters across all queues
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/jobs.KPI'
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"500":
|
||||||
|
description: internal error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Jobs KPI
|
||||||
|
tags:
|
||||||
|
- jobs
|
||||||
|
/api/v1/jobs/queues:
|
||||||
|
get:
|
||||||
|
description: Counts and avg duration per queue (last 24h)
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/jobs.QueueRollup'
|
||||||
|
type: array
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"500":
|
||||||
|
description: internal error
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Per-queue rollups
|
||||||
|
tags:
|
||||||
|
- jobs
|
||||||
/api/v1/labels:
|
/api/v1/labels:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
35
go.mod
35
go.mod
@@ -11,29 +11,29 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/spf13/viper v1.20.1
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/swaggo/http-swagger/v2 v2.0.2
|
github.com/swaggo/http-swagger/v2 v2.0.2
|
||||||
github.com/swaggo/swag v1.16.6
|
github.com/swaggo/swag v1.16.6
|
||||||
github.com/wneessen/go-mail v0.6.2
|
github.com/wneessen/go-mail v0.7.0
|
||||||
golang.org/x/crypto v0.41.0
|
golang.org/x/crypto v0.42.0
|
||||||
golang.org/x/text v0.28.0
|
golang.org/x/text v0.29.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/datatypes v1.2.6
|
gorm.io/datatypes v1.2.7
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.30.2
|
gorm.io/gorm v1.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||||
github.com/go-openapi/spec v0.20.6 // indirect
|
github.com/go-openapi/spec v0.20.6 // indirect
|
||||||
github.com/go-openapi/swag v0.19.15 // indirect
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
@@ -46,21 +46,20 @@ require (
|
|||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/mailru/easyjson v0.7.6 // indirect
|
github.com/mailru/easyjson v0.7.6 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.12.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.7.1 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
|
||||||
golang.org/x/mod v0.27.0 // indirect
|
golang.org/x/mod v0.27.0 // indirect
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
golang.org/x/tools v0.36.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gorm.io/driver/mysql v1.5.6 // indirect
|
gorm.io/driver/mysql v1.5.6 // indirect
|
||||||
|
|||||||
149
go.sum
149
go.sum
@@ -16,8 +16,8 @@ github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/
|
|||||||
github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw=
|
github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||||
@@ -35,8 +35,8 @@ github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/
|
|||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
@@ -45,7 +45,6 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
|
|||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -81,41 +80,42 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
|
|||||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
||||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||||
@@ -124,89 +124,24 @@ github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSy
|
|||||||
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
|
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
|
||||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||||
github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8=
|
github.com/wneessen/go-mail v0.7.0 h1:/Wmgd5AVjp5PA+Ken5EFfr+QR83gmqHli9HcAhh0vnU=
|
||||||
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
|
github.com/wneessen/go-mail v0.7.0/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|
||||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
|
||||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
|
||||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
|
||||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -219,16 +154,16 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/datatypes v1.2.6 h1:KafLdXvFUhzNeL2ncm03Gl3eTLONQfNKZ+wJ+9Y4Nck=
|
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
||||||
gorm.io/datatypes v1.2.6/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||||
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||||
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
|
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
|
||||||
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
|
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
|
||||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
gorm.io/gorm v1.30.2 h1:f7bevlVoVe4Byu3pmbWPVHnPsLoWaMjEb7/clyr9Ivs=
|
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||||
gorm.io/gorm v1.30.2/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/glueops/autoglue/internal/handlers/authn"
|
"github.com/glueops/autoglue/internal/handlers/authn"
|
||||||
"github.com/glueops/autoglue/internal/handlers/clusters"
|
"github.com/glueops/autoglue/internal/handlers/clusters"
|
||||||
"github.com/glueops/autoglue/internal/handlers/health"
|
"github.com/glueops/autoglue/internal/handlers/health"
|
||||||
|
"github.com/glueops/autoglue/internal/handlers/jobs"
|
||||||
"github.com/glueops/autoglue/internal/handlers/labels"
|
"github.com/glueops/autoglue/internal/handlers/labels"
|
||||||
"github.com/glueops/autoglue/internal/handlers/nodepools"
|
"github.com/glueops/autoglue/internal/handlers/nodepools"
|
||||||
"github.com/glueops/autoglue/internal/handlers/orgs"
|
"github.com/glueops/autoglue/internal/handlers/orgs"
|
||||||
@@ -36,6 +37,17 @@ func RegisterRoutes(r chi.Router) {
|
|||||||
ad.Delete("/users/{userId}", authn.AdminDeleteUser)
|
ad.Delete("/users/{userId}", authn.AdminDeleteUser)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
v1.Route("/jobs", func(j chi.Router) {
|
||||||
|
j.Use(authMW)
|
||||||
|
j.Get("/kpi", jobs.GetKPI)
|
||||||
|
j.Get("/queues", jobs.GetQueues)
|
||||||
|
j.Get("/active", jobs.GetActive)
|
||||||
|
j.Get("/failures", jobs.GetFailures)
|
||||||
|
j.Post("/{id}/retry", jobs.RetryNow)
|
||||||
|
j.Post("/{id}/cancel", jobs.Cancel)
|
||||||
|
j.Post("/{id}/enqueue", jobs.Enqueue)
|
||||||
|
})
|
||||||
|
|
||||||
v1.Route("/auth", func(a chi.Router) {
|
v1.Route("/auth", func(a chi.Router) {
|
||||||
a.Post("/login", authn.Login)
|
a.Post("/login", authn.Login)
|
||||||
a.Post("/register", authn.Register)
|
a.Post("/register", authn.Register)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// internal/bg/bg.go
|
|
||||||
package bg
|
package bg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,10 +10,13 @@ import (
|
|||||||
|
|
||||||
"github.com/dyaksa/archer"
|
"github.com/dyaksa/archer"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Jobs struct{ Client *archer.Client }
|
type Jobs struct{ Client *archer.Client }
|
||||||
|
|
||||||
|
var BgJobs *Jobs
|
||||||
|
|
||||||
func archerOptionsFromDSN(dsn string) (*archer.Options, error) {
|
func archerOptionsFromDSN(dsn string) (*archer.Options, error) {
|
||||||
u, err := url.Parse(dsn)
|
u, err := url.Parse(dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -40,7 +42,7 @@ func archerOptionsFromDSN(dsn string) (*archer.Options, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJobs() (*Jobs, error) {
|
func NewJobs(gdb *gorm.DB) (*Jobs, error) {
|
||||||
opts, err := archerOptionsFromDSN(viper.GetString("database.dsn"))
|
opts, err := archerOptionsFromDSN(viper.GetString("database.dsn"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -54,6 +56,10 @@ func NewJobs() (*Jobs, error) {
|
|||||||
if timeoutSec <= 0 {
|
if timeoutSec <= 0 {
|
||||||
timeoutSec = 60
|
timeoutSec = 60
|
||||||
}
|
}
|
||||||
|
retainDays := viper.GetInt("archer.cleanup_retain_days")
|
||||||
|
if retainDays <= 0 {
|
||||||
|
retainDays = 7
|
||||||
|
}
|
||||||
|
|
||||||
// LOG what we’re connecting to (sanitized) so you can confirm DB/host
|
// LOG what we’re connecting to (sanitized) so you can confirm DB/host
|
||||||
log.Printf("[archer] addr=%s db=%s user=%s ssl=%s", opts.Addr, opts.DBName, opts.User, opts.SSL)
|
log.Printf("[archer] addr=%s db=%s user=%s ssl=%s", opts.Addr, opts.DBName, opts.User, opts.SSL)
|
||||||
@@ -74,7 +80,15 @@ func NewJobs() (*Jobs, error) {
|
|||||||
archer.WithTimeout(time.Duration(timeoutSec)*time.Second),
|
archer.WithTimeout(time.Duration(timeoutSec)*time.Second),
|
||||||
)
|
)
|
||||||
|
|
||||||
return &Jobs{Client: c}, nil
|
jobs := &Jobs{Client: c}
|
||||||
|
|
||||||
|
c.Register(
|
||||||
|
"archer_cleanup",
|
||||||
|
CleanupWorker(gdb, jobs, retainDays),
|
||||||
|
archer.WithInstances(1),
|
||||||
|
archer.WithTimeout(5*time.Minute),
|
||||||
|
)
|
||||||
|
return jobs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *Jobs) Start() error { return j.Client.Start() }
|
func (j *Jobs) Start() error { return j.Client.Start() }
|
||||||
|
|||||||
53
internal/bg/cleanup.go
Normal file
53
internal/bg/cleanup.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package bg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dyaksa/archer"
|
||||||
|
"github.com/dyaksa/archer/job"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CleanupArgs struct {
|
||||||
|
RetainDays int `json:"retain_days"`
|
||||||
|
Table string `json:"table"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobRow struct {
|
||||||
|
ID string `gorm:"primaryKey"`
|
||||||
|
Status string
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (JobRow) TableName() string { return "jobs" }
|
||||||
|
|
||||||
|
func CleanupWorker(gdb *gorm.DB, jobs *Jobs, retainDays int) archer.WorkerFn {
|
||||||
|
return func(ctx context.Context, j job.Job) (any, error) {
|
||||||
|
if err := CleanupJobs(gdb, retainDays); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// schedule tomorrow 03:30
|
||||||
|
next := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour).Add(3*time.Hour + 30*time.Minute)
|
||||||
|
|
||||||
|
_, _ = jobs.Enqueue(
|
||||||
|
ctx,
|
||||||
|
uuid.NewString(),
|
||||||
|
"archer_cleanup",
|
||||||
|
CleanupArgs{RetainDays: retainDays, Table: "jobs"},
|
||||||
|
archer.WithScheduleTime(next),
|
||||||
|
archer.WithMaxRetries(1),
|
||||||
|
)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanupJobs(db *gorm.DB, retainDays int) error {
|
||||||
|
cutoff := time.Now().AddDate(0, 0, -retainDays)
|
||||||
|
return db.
|
||||||
|
Where("status IN ?", []string{"success", "failed", "cancelled"}).
|
||||||
|
Where("updated_at < ?", cutoff).
|
||||||
|
Delete(&JobRow{}).Error
|
||||||
|
}
|
||||||
293
internal/handlers/jobs/jobs.go
Normal file
293
internal/handlers/jobs/jobs.go
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
package jobs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dyaksa/archer"
|
||||||
|
"github.com/glueops/autoglue/internal/bg"
|
||||||
|
"github.com/glueops/autoglue/internal/db"
|
||||||
|
"github.com/glueops/autoglue/internal/db/models"
|
||||||
|
"github.com/glueops/autoglue/internal/middleware"
|
||||||
|
"github.com/glueops/autoglue/internal/response"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JobListItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
QueueName string `json:"queue_name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
RetryCount int `json:"retry_count"`
|
||||||
|
MaxRetry int `json:"max_retry"`
|
||||||
|
ScheduledAt time.Time `json:"scheduled_at"`
|
||||||
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
LastError *string `json:"last_error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnqueueReq struct {
|
||||||
|
Queue string `json:"queue"`
|
||||||
|
Args json.RawMessage `json:"args"` // keep raw and pass through to Archer
|
||||||
|
MaxRetries *int `json:"max_retries,omitempty"`
|
||||||
|
ScheduleAt *time.Time `json:"schedule_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnqueueResp struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLimit(r *http.Request, def int) int {
|
||||||
|
if s := r.URL.Query().Get("limit"); s != "" {
|
||||||
|
if n, err := strconv.Atoi(s); err == nil && n > 0 && n <= 1000 {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNotFoundErr(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
msg := err.Error()
|
||||||
|
return msg == "job not found" || msg == "no rows in result set"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- READ ENDPOINTS ----------------------
|
||||||
|
|
||||||
|
// GetKPI godoc
|
||||||
|
// @Summary Jobs KPI
|
||||||
|
// @Description Aggregated counters across all queues
|
||||||
|
// @Tags jobs
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} jobs.KPI
|
||||||
|
// @Failure 401 {string} string "unauthorized"
|
||||||
|
// @Failure 500 {string} string "internal error"
|
||||||
|
// @Router /api/v1/jobs/kpi [get]
|
||||||
|
func GetKPI(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if middleware.GetAuthContext(r) == nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
k, err := LoadKPI(db.DB)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = response.JSON(w, http.StatusOK, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueues godoc
|
||||||
|
// @Summary Per-queue rollups
|
||||||
|
// @Description Counts and avg duration per queue (last 24h)
|
||||||
|
// @Tags jobs
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} jobs.QueueRollup
|
||||||
|
// @Failure 401 {string} string "unauthorized"
|
||||||
|
// @Failure 500 {string} string "internal error"
|
||||||
|
// @Router /api/v1/jobs/queues [get]
|
||||||
|
func GetQueues(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if middleware.GetAuthContext(r) == nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := LoadPerQueue(db.DB)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = response.JSON(w, http.StatusOK, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActive godoc
|
||||||
|
// @Summary Active jobs
|
||||||
|
// @Description Currently running jobs (limit default 100)
|
||||||
|
// @Tags jobs
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Max rows" default(100)
|
||||||
|
// @Success 200 {array} jobs.JobListItem
|
||||||
|
// @Failure 401 {string} string "unauthorized"
|
||||||
|
// @Failure 500 {string} string "internal error"
|
||||||
|
// @Router /api/v1/jobs/active [get]
|
||||||
|
func GetActive(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if middleware.GetAuthContext(r) == nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := parseLimit(r, 100)
|
||||||
|
|
||||||
|
var rows []JobListItem
|
||||||
|
err := db.DB.Model(&models.Job{}).
|
||||||
|
Select("id, queue_name, status, retry_count, max_retry, scheduled_at, started_at, updated_at, last_error").
|
||||||
|
Where("status = ?", "running").
|
||||||
|
Order("started_at DESC NULLS LAST, updated_at DESC").
|
||||||
|
Limit(limit).
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = response.JSON(w, http.StatusOK, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFailures godoc
|
||||||
|
// @Summary Recent failures
|
||||||
|
// @Description Failed jobs ordered by most recent (limit default 100)
|
||||||
|
// @Tags jobs
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Max rows" default(100)
|
||||||
|
// @Success 200 {array} jobs.JobListItem
|
||||||
|
// @Failure 401 {string} string "unauthorized"
|
||||||
|
// @Failure 500 {string} string "internal error"
|
||||||
|
// @Router /api/v1/jobs/failures [get]
|
||||||
|
func GetFailures(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if middleware.GetAuthContext(r) == nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := parseLimit(r, 100)
|
||||||
|
|
||||||
|
var rows []JobListItem
|
||||||
|
err := db.DB.Model(&models.Job{}).
|
||||||
|
Select("id, queue_name, status, retry_count, max_retry, scheduled_at, started_at, updated_at, last_error").
|
||||||
|
Where("status = ?", "failed").
|
||||||
|
Order("updated_at DESC").
|
||||||
|
Limit(limit).
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = response.JSON(w, http.StatusOK, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- MUTATION ENDPOINTS ----------------------
|
||||||
|
|
||||||
|
// RetryNow godoc
|
||||||
|
// @Summary Retry a job immediately
|
||||||
|
// @Description Calls Archer ScheduleNow on the job id
|
||||||
|
// @Tags jobs
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Job ID"
|
||||||
|
// @Success 204 {string} string "no content"
|
||||||
|
// @Failure 400 {string} string "bad request"
|
||||||
|
// @Failure 401 {string} string "unauthorized"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "internal error"
|
||||||
|
// @Router /api/v1/jobs/{id}/retry [post]
|
||||||
|
func RetryNow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if middleware.GetAuthContext(r) == nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
if id == "" {
|
||||||
|
http.Error(w, "missing id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// archer.ScheduleNow returns (any, error); if the id is unknown, expect an error you can surface as 404
|
||||||
|
if _, err := bg.BgJobs.Client.ScheduleNow(r.Context(), id); err != nil {
|
||||||
|
status := http.StatusInternalServerError
|
||||||
|
// (Optional) map error text if Archer returns a recognizable "not found"
|
||||||
|
if isNotFoundErr(err) {
|
||||||
|
status = http.StatusNotFound
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.NoContent(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel godoc
|
||||||
|
// @Summary Cancel a job
|
||||||
|
// @Description Cancels running or scheduled jobs
|
||||||
|
// @Tags jobs
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Job ID"
|
||||||
|
// @Success 204 {string} string "no content"
|
||||||
|
// @Failure 400 {string} string "bad request"
|
||||||
|
// @Failure 401 {string} string "unauthorized"
|
||||||
|
// @Failure 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "internal error"
|
||||||
|
// @Router /api/v1/jobs/{id}/cancel [post]
|
||||||
|
func Cancel(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if middleware.GetAuthContext(r) == nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
if id == "" {
|
||||||
|
http.Error(w, "missing id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := bg.BgJobs.Client.Cancel(r.Context(), id); err != nil {
|
||||||
|
status := http.StatusInternalServerError
|
||||||
|
if isNotFoundErr(err) {
|
||||||
|
status = http.StatusNotFound
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.NoContent(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue godoc
|
||||||
|
// @Summary Manually enqueue a job
|
||||||
|
// @Description Schedules a job on a queue with optional args/schedule
|
||||||
|
// @Tags jobs
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body jobs.EnqueueReq true "Enqueue request"
|
||||||
|
// @Success 202 {object} jobs.EnqueueResp
|
||||||
|
// @Failure 400 {string} string "bad request"
|
||||||
|
// @Failure 401 {string} string "unauthorized"
|
||||||
|
// @Failure 500 {string} string "internal error"
|
||||||
|
// @Router /api/v1/jobs/enqueue [post]
|
||||||
|
func Enqueue(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if middleware.GetAuthContext(r) == nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req EnqueueReq
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid json: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Queue == "" {
|
||||||
|
http.Error(w, "queue is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := uuid.NewString()
|
||||||
|
opts := []archer.FnOptions{}
|
||||||
|
if req.MaxRetries != nil {
|
||||||
|
opts = append(opts, archer.WithMaxRetries(*req.MaxRetries))
|
||||||
|
}
|
||||||
|
if req.ScheduleAt != nil {
|
||||||
|
opts = append(opts, archer.WithScheduleTime(*req.ScheduleAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := bg.BgJobs.Client.Schedule(r.Context(), id, req.Queue, req.Args, opts...); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = response.JSON(w, http.StatusAccepted, EnqueueResp{ID: id})
|
||||||
|
}
|
||||||
66
internal/handlers/jobs/kpi.go
Normal file
66
internal/handlers/jobs/kpi.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package jobs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/db/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KPI struct {
|
||||||
|
RunningNow int64
|
||||||
|
DueNow int64
|
||||||
|
ScheduledFuture int64
|
||||||
|
Succeeded24h int64
|
||||||
|
Failed24h int64
|
||||||
|
Retryable int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadKPI(db *gorm.DB) (KPI, error) {
|
||||||
|
var k KPI
|
||||||
|
now := time.Now()
|
||||||
|
dayAgo := now.Add(-24 * time.Hour)
|
||||||
|
|
||||||
|
if err := db.Model(&models.Job{}).
|
||||||
|
Where("status = ?", "running").
|
||||||
|
Count(&k.RunningNow).Error; err != nil {
|
||||||
|
return k, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Model(&models.Job{}).
|
||||||
|
Where("status IN ?", []string{"queued", "scheduled", "pending"}).
|
||||||
|
Where("scheduled_at > ?", now).
|
||||||
|
Count(&k.ScheduledFuture).Error; err != nil {
|
||||||
|
return k, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Model(&models.Job{}).
|
||||||
|
Where("status IN ?", []string{"queued", "scheduled", "pending"}).
|
||||||
|
Where("scheduled_at <= ?", now).
|
||||||
|
Count(&k.DueNow).Error; err != nil {
|
||||||
|
return k, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Model(&models.Job{}).
|
||||||
|
Where("status = ?", "success").
|
||||||
|
Where("updated_at >= ?", dayAgo).
|
||||||
|
Count(&k.Succeeded24h).Error; err != nil {
|
||||||
|
return k, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Model(&models.Job{}).
|
||||||
|
Where("status = ?", "failed").
|
||||||
|
Where("updated_at >= ?", dayAgo).
|
||||||
|
Count(&k.Failed24h).Error; err != nil {
|
||||||
|
return k, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Model(&models.Job{}).
|
||||||
|
Where("status = ?", "failed").
|
||||||
|
Where("retry_count < max_retry").
|
||||||
|
Count(&k.Retryable).Error; err != nil {
|
||||||
|
return k, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return k, nil
|
||||||
|
}
|
||||||
64
internal/handlers/jobs/queue.go
Normal file
64
internal/handlers/jobs/queue.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package jobs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glueops/autoglue/internal/db/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QueueRollup struct {
|
||||||
|
QueueName string
|
||||||
|
Running int64
|
||||||
|
QueuedDue int64
|
||||||
|
QueuedFuture int64
|
||||||
|
Success24h int64
|
||||||
|
Failed24h int64
|
||||||
|
AvgDurationSecs float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadPerQueue(db *gorm.DB) ([]QueueRollup, error) {
|
||||||
|
var queues []string
|
||||||
|
if err := db.Model(&models.Job{}).Distinct().Pluck("queue_name", &queues).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
dayAgo := now.Add(-24 * time.Hour)
|
||||||
|
out := make([]QueueRollup, 0, len(queues))
|
||||||
|
|
||||||
|
for _, q := range queues {
|
||||||
|
var rr, qd, qf, s24, f24 int64
|
||||||
|
var avgDur *float64
|
||||||
|
|
||||||
|
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status = 'running'", q).Count(&rr).Error
|
||||||
|
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status IN ('queued','scheduled','pending') AND scheduled_at <= ?", q, now).Count(&qd).Error
|
||||||
|
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status IN ('queued','scheduled','pending') AND scheduled_at > ?", q, now).Count(&qf).Error
|
||||||
|
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status = 'success' AND updated_at >= ?", q, dayAgo).Count(&s24).Error
|
||||||
|
_ = db.Model(&models.Job{}).Where("queue_name = ? AND status = 'failed' AND updated_at >= ?", q, dayAgo).Count(&f24).Error
|
||||||
|
|
||||||
|
_ = db.
|
||||||
|
Model(&models.Job{}).
|
||||||
|
Select("AVG(EXTRACT(EPOCH FROM (updated_at - started_at)))").
|
||||||
|
Where("queue_name = ? AND status = 'success' AND started_at IS NOT NULL AND updated_at >= ?", q, dayAgo).
|
||||||
|
Scan(&avgDur).Error
|
||||||
|
|
||||||
|
out = append(out, QueueRollup{
|
||||||
|
QueueName: q,
|
||||||
|
Running: rr,
|
||||||
|
QueuedDue: qd,
|
||||||
|
QueuedFuture: qf,
|
||||||
|
Success24h: s24,
|
||||||
|
Failed24h: f24,
|
||||||
|
AvgDurationSecs: coalesceF64(avgDur, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func coalesceF64(p *float64, d float64) float64 {
|
||||||
|
if p == nil {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
return *p
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
1
internal/ui/dist/assets/index--a4aJrTK.css
vendored
1
internal/ui/dist/assets/index--a4aJrTK.css
vendored
File diff suppressed because one or more lines are too long
1
internal/ui/dist/assets/index-CHoyJPs-.css
vendored
Normal file
1
internal/ui/dist/assets/index-CHoyJPs-.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
internal/ui/dist/assets/index-CmZFDWt2.js
vendored
1
internal/ui/dist/assets/index-CmZFDWt2.js
vendored
File diff suppressed because one or more lines are too long
1
internal/ui/dist/assets/index-DSxuk_EI.js
vendored
Normal file
1
internal/ui/dist/assets/index-DSxuk_EI.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
internal/ui/dist/index.html
vendored
6
internal/ui/dist/index.html
vendored
@@ -5,12 +5,12 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>AutoGlue</title>
|
<title>AutoGlue</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CmZFDWt2.js"></script>
|
<script type="module" crossorigin src="/assets/index-DSxuk_EI.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/router-CANfZtzM.js">
|
<link rel="modulepreload" crossorigin href="/assets/router-CANfZtzM.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-DvippHRz.js">
|
<link rel="modulepreload" crossorigin href="/assets/vendor-DvippHRz.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-DRmH1vcw.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-DRmH1vcw.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/icons-DQ1I1M7X.js">
|
<link rel="modulepreload" crossorigin href="/assets/icons-B5E6SSBo.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index--a4aJrTK.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CHoyJPs-.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { NotFoundPage } from "@/pages/error/not-found.tsx"
|
|||||||
import { SshKeysPage } from "@/pages/security/ssh.tsx"
|
import { SshKeysPage } from "@/pages/security/ssh.tsx"
|
||||||
import { MemberManagement } from "@/pages/settings/members.tsx"
|
import { MemberManagement } from "@/pages/settings/members.tsx"
|
||||||
import { OrgManagement } from "@/pages/settings/orgs.tsx"
|
import { OrgManagement } from "@/pages/settings/orgs.tsx"
|
||||||
|
import JobsDashboard from "@/pages/settings/jobs.tsx";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -57,6 +58,7 @@ function App() {
|
|||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/settings">
|
<Route path="/settings">
|
||||||
|
<Route path="jobs" element={<JobsDashboard />} />
|
||||||
<Route path="orgs" element={<OrgManagement />} />
|
<Route path="orgs" element={<OrgManagement />} />
|
||||||
<Route path="members" element={<MemberManagement />} />
|
<Route path="members" element={<MemberManagement />} />
|
||||||
<Route path="me" element={<Me />} />
|
<Route path="me" element={<Me />} />
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
UsersIcon,
|
UsersIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { AiOutlineCluster } from "react-icons/ai"
|
import { AiOutlineCluster } from "react-icons/ai"
|
||||||
|
import {GrUserWorker} from "react-icons/gr";
|
||||||
|
|
||||||
export type NavItem = {
|
export type NavItem = {
|
||||||
label: string
|
label: string
|
||||||
@@ -95,6 +96,11 @@ export const items = [
|
|||||||
label: "Settings",
|
label: "Settings",
|
||||||
icon: SettingsIcon,
|
icon: SettingsIcon,
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
label: "Jobs",
|
||||||
|
icon: GrUserWorker,
|
||||||
|
to: '/settings/jobs',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Organizations",
|
label: "Organizations",
|
||||||
to: "/settings/orgs",
|
to: "/settings/orgs",
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ const CreateClusterSchema = z.object({
|
|||||||
name: z.string().trim().min(2, "Name is too short"),
|
name: z.string().trim().min(2, "Name is too short"),
|
||||||
provider: z.string().trim().min(2, "Provider is too short"),
|
provider: z.string().trim().min(2, "Provider is too short"),
|
||||||
region: z.string().trim().min(1, "Region is required"),
|
region: z.string().trim().min(1, "Region is required"),
|
||||||
node_pool_ids: z.array(z.string().uuid()).optional().default([]),
|
node_pool_ids: z.array(z.uuid()).default([]).optional(),
|
||||||
bastion_server_id: z.string().uuid().optional(),
|
bastion_server_id: z.uuid().optional(),
|
||||||
cluster_load_balancer: z.string().optional(),
|
cluster_load_balancer: z.string().optional(),
|
||||||
control_load_balancer: z.string().optional(),
|
control_load_balancer: z.string().optional(),
|
||||||
kubeconfig: z.string().optional(),
|
kubeconfig: z.string().optional(),
|
||||||
@@ -88,7 +88,7 @@ const UpdateClusterSchema = z
|
|||||||
provider: z.string().trim().min(2, "Provider is too short").optional(),
|
provider: z.string().trim().min(2, "Provider is too short").optional(),
|
||||||
region: z.string().trim().min(1, "Region is required").optional(),
|
region: z.string().trim().min(1, "Region is required").optional(),
|
||||||
status: z.string().trim().min(1, "Status is required").optional(),
|
status: z.string().trim().min(1, "Status is required").optional(),
|
||||||
bastion_server_id: z.string().uuid().or(z.literal("")).optional(),
|
bastion_server_id: z.uuid().or(z.literal("")).optional(),
|
||||||
cluster_load_balancer: z.string().optional(),
|
cluster_load_balancer: z.string().optional(),
|
||||||
control_load_balancer: z.string().optional(),
|
control_load_balancer: z.string().optional(),
|
||||||
kubeconfig: z.string().optional(),
|
kubeconfig: z.string().optional(),
|
||||||
@@ -113,7 +113,7 @@ const AttachPoolsSchema = z.object({
|
|||||||
export type AttachPoolsValues = z.infer<typeof AttachPoolsSchema>
|
export type AttachPoolsValues = z.infer<typeof AttachPoolsSchema>
|
||||||
|
|
||||||
const SetBastionSchema = z.object({
|
const SetBastionSchema = z.object({
|
||||||
server_id: z.string().uuid({ message: "Enter a valid Server UUID" }),
|
server_id: z.uuid({ message: "Enter a valid Server UUID" }),
|
||||||
})
|
})
|
||||||
export type SetBastionValues = z.infer<typeof SetBastionSchema>
|
export type SetBastionValues = z.infer<typeof SetBastionSchema>
|
||||||
|
|
||||||
|
|||||||
189
ui/src/pages/settings/jobs.tsx
Normal file
189
ui/src/pages/settings/jobs.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import {useCallback, useEffect, useMemo, useState} from "react";
|
||||||
|
import {api} from "@/lib/api.ts";
|
||||||
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card.tsx";
|
||||||
|
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table.tsx";
|
||||||
|
|
||||||
|
export interface KPI {
|
||||||
|
RunningNow: number;
|
||||||
|
DueNow: number;
|
||||||
|
ScheduledFuture: number;
|
||||||
|
Succeeded24h: number;
|
||||||
|
Failed24h: number;
|
||||||
|
Retryable: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueueRollup {
|
||||||
|
QueueName: string;
|
||||||
|
Running: number;
|
||||||
|
QueuedDue: number;
|
||||||
|
QueuedFuture: number;
|
||||||
|
Success24h: number;
|
||||||
|
Failed24h: number;
|
||||||
|
AvgDurationSecs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface JobListItem {
|
||||||
|
id: string;
|
||||||
|
queue_name: string;
|
||||||
|
status: string;
|
||||||
|
retry_count: number;
|
||||||
|
max_retry: number;
|
||||||
|
scheduled_at: string; // ISO
|
||||||
|
started_at?: string; // ISO
|
||||||
|
updated_at: string; // ISO
|
||||||
|
last_error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const fmtNumber = (n: number | undefined) => (n ?? 0).toLocaleString();
|
||||||
|
|
||||||
|
|
||||||
|
const fmtSeconds = (secs: number) => {
|
||||||
|
if (!isFinite(secs) || secs <= 0) return "–";
|
||||||
|
if (secs < 60) return `${secs.toFixed(0)}s`;
|
||||||
|
if (secs < 3600) return `${Math.floor(secs / 60)}m ${Math.floor(secs % 60)}s`;
|
||||||
|
const h = Math.floor(secs / 3600);
|
||||||
|
const m = Math.floor((secs % 3600) / 60);
|
||||||
|
return `${h}h ${m}m`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function JobsDashboard() {
|
||||||
|
const [kpi, setKpi] = useState<KPI | null>(null);
|
||||||
|
const [queues, setQueues] = useState<QueueRollup[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||||
|
const [refreshMs, setRefreshMs] = useState(5000);
|
||||||
|
|
||||||
|
const loadAll = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const [k, q] = await Promise.all([
|
||||||
|
api.get<KPI>("/api/v1/jobs/kpi"),
|
||||||
|
api.get<QueueRollup[]>("/api/v1/jobs/queues"),
|
||||||
|
])
|
||||||
|
setKpi(k)
|
||||||
|
setQueues(q)
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || String(e));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => { void loadAll(); }, [loadAll]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!autoRefresh) return;
|
||||||
|
const id = setInterval(loadAll, refreshMs);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [autoRefresh, refreshMs, loadAll]);
|
||||||
|
|
||||||
|
const totals = useMemo(() => ({
|
||||||
|
queues: queues.length,
|
||||||
|
running: queues.reduce((s, q) => s + q.Running, 0),
|
||||||
|
due: queues.reduce((s, q) => s + q.QueuedDue, 0),
|
||||||
|
future: queues.reduce((s, q) => s + q.QueuedFuture, 0),
|
||||||
|
}), [queues]);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
<header className="flex items-center justify-between gap-3">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">Jobs Dashboard</h1>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
<input type="checkbox" className="h-4 w-4" checked={autoRefresh} onChange={(e)=>setAutoRefresh(e.target.checked)} />
|
||||||
|
Auto refresh
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="border rounded px-2 py-1 text-sm"
|
||||||
|
value={refreshMs}
|
||||||
|
onChange={(e)=>setRefreshMs(parseInt(e.target.value))}
|
||||||
|
>
|
||||||
|
<option value={3000}>3s</option>
|
||||||
|
<option value={5000}>5s</option>
|
||||||
|
<option value={10000}>10s</option>
|
||||||
|
<option value={30000}>30s</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="px-3 py-1.5 rounded bg-slate-900 text-white text-sm hover:opacity-90"
|
||||||
|
onClick={loadAll}
|
||||||
|
disabled={loading}
|
||||||
|
>{loading ? "Refreshing…" : "Refresh"}</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="rounded border border-red-300 bg-red-50 text-red-800 p-3 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* KPI cards */}
|
||||||
|
<section className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-6">
|
||||||
|
<KpiCard label="Running" value={fmtNumber(kpi?.RunningNow)} />
|
||||||
|
<KpiCard label="Due now" value={fmtNumber(kpi?.DueNow)} />
|
||||||
|
<KpiCard label="Scheduled" value={fmtNumber(kpi?.ScheduledFuture)} />
|
||||||
|
<KpiCard label="Succeeded (24h)" value={fmtNumber(kpi?.Succeeded24h)} />
|
||||||
|
<KpiCard label="Failed (24h)" value={fmtNumber(kpi?.Failed24h)} />
|
||||||
|
<KpiCard label="Retryable" value={fmtNumber(kpi?.Retryable)} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Per-queue table */}
|
||||||
|
<section className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-medium">Queues <span className="text-slate-500 text-sm">({totals.queues})</span></h2>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto rounded border">
|
||||||
|
<Table className="min-w-full text-sm">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Queue</TableHead>
|
||||||
|
<TableHead className='text-right'>Running</TableHead>
|
||||||
|
<TableHead className='text-right'>Due</TableHead>
|
||||||
|
<TableHead className='text-right'>Future</TableHead>
|
||||||
|
<TableHead className='text-right'>Success 24h</TableHead>
|
||||||
|
<TableHead className='text-right'>Failed 24h</TableHead>
|
||||||
|
<TableHead className='text-right'>Avg Duration</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{queues.map((q) => (
|
||||||
|
<TableRow key={q.QueueName} className="border-t">
|
||||||
|
<TableCell>{q.QueueName}</TableCell>
|
||||||
|
<TableCell className='text-right'>{q.Running}</TableCell>
|
||||||
|
<TableCell className='text-right'>{q.QueuedDue}</TableCell>
|
||||||
|
<TableCell className='text-right'>{q.QueuedFuture}</TableCell>
|
||||||
|
<TableCell className='text-right'>{q.Success24h}</TableCell>
|
||||||
|
<TableCell className='text-right'>{q.Failed24h}</TableCell>
|
||||||
|
<TableCell className='text-right'>{fmtSeconds(q.AvgDurationSecs)}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* <ManualEnqueue onSubmitted={loadAll} /> */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function KpiCard({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{label}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="mt-1 text-2xl font-semibold">{value}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user