Merge pull request #73 from ClusterCockpit/feature-62-rest-endpoint-delete-jobs

Feature 62 rest endpoint delete jobs
Fix #62
This commit is contained in:
Jan Eitzinger 2022-11-30 14:48:44 +01:00 committed by GitHub
commit c7eaccd3b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1102 additions and 219 deletions

View File

@ -1,9 +1,8 @@
{
"swagger": "2.0",
"info": {
"description": "Defines a tag using name and type.",
"description": "API for batch job control.",
"title": "ClusterCockpit REST API",
"termsOfService": "https://monitoring.nhr.fau.de/imprint",
"contact": {
"name": "ClusterCockpit Project",
"url": "https://github.com/ClusterCockpit",
@ -13,9 +12,9 @@
"name": "MIT License",
"url": "https://opensource.org/licenses/MIT"
},
"version": "0.1.0"
"version": "0.2.0"
},
"host": "clustercockpit.localhost:8082",
"host": "localhost:8080",
"basePath": "/api",
"paths": {
"/jobs/": {
@ -26,12 +25,12 @@
}
],
"description": "Get a list of all jobs. Filters can be applied using query parameters.\nNumber of results can be limited by page. Results are sorted by descending startTime.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"query"
],
"summary": "Lists all jobs",
"parameters": [
{
@ -62,13 +61,13 @@
},
{
"type": "integer",
"description": "Items per page (If empty: No Limit)",
"description": "Items per page (Default: 25)",
"name": "items-per-page",
"in": "query"
},
{
"type": "integer",
"description": "Page Number (If empty: No Paging)",
"description": "Page Number (Default: 1)",
"name": "page",
"in": "query"
},
@ -110,6 +109,221 @@
}
}
},
"/jobs/delete_job/": {
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Job to delete is specified by request body. All fields are required in this case.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"remove"
],
"summary": "Remove a job from the sql database",
"parameters": [
{
"description": "All fields required",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.DeleteJobApiRequest"
}
}
],
"responses": {
"200": {
"description": "Success message",
"schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
}
}
}
},
"/jobs/delete_job/{id}": {
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Job to remove is specified by database ID. This will not remove the job from the job archive.",
"produces": [
"application/json"
],
"tags": [
"remove"
],
"summary": "Remove a job from the sql database",
"parameters": [
{
"type": "integer",
"description": "Database ID of Job",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Success message",
"schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
}
}
}
},
"/jobs/delete_job_before/{ts}": {
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Remove all jobs with start time before timestamp. The jobs will not be removed from the job archive.",
"produces": [
"application/json"
],
"tags": [
"remove"
],
"summary": "Remove a job from the sql database",
"parameters": [
{
"type": "integer",
"description": "Unix epoch timestamp",
"name": "ts",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Success message",
"schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
}
}
}
},
"/jobs/start_job/": {
"post": {
"security": [
@ -124,6 +338,9 @@
"produces": [
"application/json"
],
"tags": [
"add and modify"
],
"summary": "Adds a new job as \"running\"",
"parameters": [
{
@ -187,6 +404,9 @@
"produces": [
"application/json"
],
"tags": [
"add and modify"
],
"summary": "Marks job as completed and triggers archiving",
"parameters": [
{
@ -201,7 +421,7 @@
],
"responses": {
"200": {
"description": "Job resource",
"description": "Success message",
"schema": {
"$ref": "#/definitions/schema.JobMeta"
}
@ -259,6 +479,9 @@
"produces": [
"application/json"
],
"tags": [
"add and modify"
],
"summary": "Marks job as completed and triggers archiving",
"parameters": [
{
@ -338,6 +561,9 @@
"produces": [
"application/json"
],
"tags": [
"add and modify"
],
"summary": "Adds one or more tags to a job",
"parameters": [
{
@ -355,7 +581,7 @@
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/api.Tag"
"$ref": "#/definitions/api.ApiTag"
}
}
}
@ -396,8 +622,53 @@
}
},
"definitions": {
"api.ApiTag": {
"type": "object",
"properties": {
"name": {
"description": "Tag Name",
"type": "string",
"example": "Testjob"
},
"type": {
"description": "Tag Type",
"type": "string",
"example": "Debug"
}
}
},
"api.DeleteJobApiRequest": {
"type": "object",
"required": [
"jobId"
],
"properties": {
"cluster": {
"description": "Cluster of job",
"type": "string",
"example": "fritz"
},
"jobId": {
"description": "Cluster Job ID of job",
"type": "integer",
"example": 123000
},
"startTime": {
"description": "Start Time of job as epoch",
"type": "integer",
"example": 1649723812
}
}
},
"api.DeleteJobApiResponse": {
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
},
"api.ErrorResponse": {
"description": "Error message as returned from backend.",
"type": "object",
"properties": {
"error": {
@ -411,7 +682,6 @@
}
},
"api.StartJobApiResponse": {
"description": "Successful job start response with database id of new job.",
"type": "object",
"properties": {
"id": {
@ -421,7 +691,6 @@
}
},
"api.StopJobApiRequest": {
"description": "Request to stop running job using stoptime and final state. They are only required if no database id was provided with endpoint.",
"type": "object",
"required": [
"jobState",
@ -462,22 +731,6 @@
}
}
},
"api.Tag": {
"description": "Defines a tag using name and type.",
"type": "object",
"properties": {
"name": {
"description": "Tag Name",
"type": "string",
"example": "Testjob"
},
"type": {
"description": "Tag Type",
"type": "string",
"example": "Debug"
}
}
},
"schema.Job": {
"description": "Information of a HPC job.",
"type": "object",
@ -831,10 +1084,14 @@
},
"securityDefinitions": {
"ApiKeyAuth": {
"description": "JWT based authentification for general API endpoint use.",
"type": "apiKey",
"name": "X-Auth-Token",
"in": "header"
}
},
"tags": [
{
"name": "Job API"
}
]
}

View File

@ -1,7 +1,39 @@
basePath: /api
definitions:
api.ApiTag:
properties:
name:
description: Tag Name
example: Testjob
type: string
type:
description: Tag Type
example: Debug
type: string
type: object
api.DeleteJobApiRequest:
properties:
cluster:
description: Cluster of job
example: fritz
type: string
jobId:
description: Cluster Job ID of job
example: 123000
type: integer
startTime:
description: Start Time of job as epoch
example: 1649723812
type: integer
required:
- jobId
type: object
api.DeleteJobApiResponse:
properties:
msg:
type: string
type: object
api.ErrorResponse:
description: Error message as returned from backend.
properties:
error:
description: Error Message
@ -11,15 +43,12 @@ definitions:
type: string
type: object
api.StartJobApiResponse:
description: Successful job start response with database id of new job.
properties:
id:
description: Database ID of new job
type: integer
type: object
api.StopJobApiRequest:
description: Request to stop running job using stoptime and final state. They
are only required if no database id was provided with endpoint.
properties:
cluster:
description: Cluster of job
@ -51,18 +80,6 @@ definitions:
- jobState
- stopTime
type: object
api.Tag:
description: Defines a tag using name and type.
properties:
name:
description: Tag Name
example: Testjob
type: string
type:
description: Tag Type
example: Debug
type: string
type: object
schema.Job:
description: Information of a HPC job.
properties:
@ -344,24 +361,21 @@ definitions:
example: Debug
type: string
type: object
host: clustercockpit.localhost:8082
host: localhost:8080
info:
contact:
email: support@clustercockpit.org
name: ClusterCockpit Project
url: https://github.com/ClusterCockpit
description: Defines a tag using name and type.
description: API for batch job control.
license:
name: MIT License
url: https://opensource.org/licenses/MIT
termsOfService: https://monitoring.nhr.fau.de/imprint
title: ClusterCockpit REST API
version: 0.1.0
version: 0.2.0
paths:
/jobs/:
get:
consumes:
- application/json
description: |-
Get a list of all jobs. Filters can be applied using query parameters.
Number of results can be limited by page. Results are sorted by descending startTime.
@ -385,11 +399,11 @@ paths:
in: query
name: start-time
type: string
- description: 'Items per page (If empty: No Limit)'
- description: 'Items per page (Default: 25)'
in: query
name: items-per-page
type: integer
- description: 'Page Number (If empty: No Paging)'
- description: 'Page Number (Default: 1)'
in: query
name: page
type: integer
@ -421,6 +435,152 @@ paths:
security:
- ApiKeyAuth: []
summary: Lists all jobs
tags:
- query
/jobs/delete_job/:
delete:
consumes:
- application/json
description: Job to delete is specified by request body. All fields are required
in this case.
parameters:
- description: All fields required
in: body
name: request
required: true
schema:
$ref: '#/definitions/api.DeleteJobApiRequest'
produces:
- application/json
responses:
"200":
description: Success message
schema:
$ref: '#/definitions/api.DeleteJobApiResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/api.ErrorResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/api.ErrorResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/api.ErrorResponse'
"404":
description: Resource not found
schema:
$ref: '#/definitions/api.ErrorResponse'
"422":
description: 'Unprocessable Entity: finding job failed: sql: no rows in
result set'
schema:
$ref: '#/definitions/api.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.ErrorResponse'
security:
- ApiKeyAuth: []
summary: Remove a job from the sql database
tags:
- remove
/jobs/delete_job/{id}:
delete:
description: Job to remove is specified by database ID. This will not remove
the job from the job archive.
parameters:
- description: Database ID of Job
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Success message
schema:
$ref: '#/definitions/api.DeleteJobApiResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/api.ErrorResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/api.ErrorResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/api.ErrorResponse'
"404":
description: Resource not found
schema:
$ref: '#/definitions/api.ErrorResponse'
"422":
description: 'Unprocessable Entity: finding job failed: sql: no rows in
result set'
schema:
$ref: '#/definitions/api.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.ErrorResponse'
security:
- ApiKeyAuth: []
summary: Remove a job from the sql database
tags:
- remove
/jobs/delete_job_before/{ts}:
delete:
description: Remove all jobs with start time before timestamp. The jobs will
not be removed from the job archive.
parameters:
- description: Unix epoch timestamp
in: path
name: ts
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Success message
schema:
$ref: '#/definitions/api.DeleteJobApiResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/api.ErrorResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/api.ErrorResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/api.ErrorResponse'
"404":
description: Resource not found
schema:
$ref: '#/definitions/api.ErrorResponse'
"422":
description: 'Unprocessable Entity: finding job failed: sql: no rows in
result set'
schema:
$ref: '#/definitions/api.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.ErrorResponse'
security:
- ApiKeyAuth: []
summary: Remove a job from the sql database
tags:
- remove
/jobs/start_job/:
post:
consumes:
@ -466,6 +626,8 @@ paths:
security:
- ApiKeyAuth: []
summary: Adds a new job as "running"
tags:
- add and modify
/jobs/stop_job/:
post:
description: |-
@ -482,7 +644,7 @@ paths:
- application/json
responses:
"200":
description: Job resource
description: Success message
schema:
$ref: '#/definitions/schema.JobMeta'
"400":
@ -513,6 +675,8 @@ paths:
security:
- ApiKeyAuth: []
summary: Marks job as completed and triggers archiving
tags:
- add and modify
/jobs/stop_job/{id}:
post:
consumes:
@ -567,6 +731,8 @@ paths:
security:
- ApiKeyAuth: []
summary: Marks job as completed and triggers archiving
tags:
- add and modify
/jobs/tag_job/{id}:
post:
consumes:
@ -586,7 +752,7 @@ paths:
required: true
schema:
items:
$ref: '#/definitions/api.Tag'
$ref: '#/definitions/api.ApiTag'
type: array
produces:
- application/json
@ -614,10 +780,13 @@ paths:
security:
- ApiKeyAuth: []
summary: Adds one or more tags to a job
tags:
- add and modify
securityDefinitions:
ApiKeyAuth:
description: JWT based authentification for general API endpoint use.
in: header
name: X-Auth-Token
type: apiKey
swagger: "2.0"
tags:
- name: Job API

View File

@ -1,6 +1,5 @@
// Package api GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag at
// 2022-09-22 13:31:53.353204065 +0200 CEST m=+0.139444562
// This file was generated by swaggo/swag
package api
import "github.com/swaggo/swag"
@ -11,7 +10,6 @@ const docTemplate = `{
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"termsOfService": "https://monitoring.nhr.fau.de/imprint",
"contact": {
"name": "ClusterCockpit Project",
"url": "https://github.com/ClusterCockpit",
@ -34,12 +32,12 @@ const docTemplate = `{
}
],
"description": "Get a list of all jobs. Filters can be applied using query parameters.\nNumber of results can be limited by page. Results are sorted by descending startTime.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"query"
],
"summary": "Lists all jobs",
"parameters": [
{
@ -70,13 +68,13 @@ const docTemplate = `{
},
{
"type": "integer",
"description": "Items per page (If empty: No Limit)",
"description": "Items per page (Default: 25)",
"name": "items-per-page",
"in": "query"
},
{
"type": "integer",
"description": "Page Number (If empty: No Paging)",
"description": "Page Number (Default: 1)",
"name": "page",
"in": "query"
},
@ -118,6 +116,221 @@ const docTemplate = `{
}
}
},
"/jobs/delete_job/": {
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Job to delete is specified by request body. All fields are required in this case.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"remove"
],
"summary": "Remove a job from the sql database",
"parameters": [
{
"description": "All fields required",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.DeleteJobApiRequest"
}
}
],
"responses": {
"200": {
"description": "Success message",
"schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
}
}
}
},
"/jobs/delete_job/{id}": {
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Job to remove is specified by database ID. This will not remove the job from the job archive.",
"produces": [
"application/json"
],
"tags": [
"remove"
],
"summary": "Remove a job from the sql database",
"parameters": [
{
"type": "integer",
"description": "Database ID of Job",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Success message",
"schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
}
}
}
},
"/jobs/delete_job_before/{ts}": {
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Remove all jobs with start time before timestamp. The jobs will not be removed from the job archive.",
"produces": [
"application/json"
],
"tags": [
"remove"
],
"summary": "Remove a job from the sql database",
"parameters": [
{
"type": "integer",
"description": "Unix epoch timestamp",
"name": "ts",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Success message",
"schema": {
"$ref": "#/definitions/api.DeleteJobApiResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"404": {
"description": "Resource not found",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"422": {
"description": "Unprocessable Entity: finding job failed: sql: no rows in result set",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.ErrorResponse"
}
}
}
}
},
"/jobs/start_job/": {
"post": {
"security": [
@ -132,6 +345,9 @@ const docTemplate = `{
"produces": [
"application/json"
],
"tags": [
"add and modify"
],
"summary": "Adds a new job as \"running\"",
"parameters": [
{
@ -195,6 +411,9 @@ const docTemplate = `{
"produces": [
"application/json"
],
"tags": [
"add and modify"
],
"summary": "Marks job as completed and triggers archiving",
"parameters": [
{
@ -209,7 +428,7 @@ const docTemplate = `{
],
"responses": {
"200": {
"description": "Job resource",
"description": "Success message",
"schema": {
"$ref": "#/definitions/schema.JobMeta"
}
@ -267,6 +486,9 @@ const docTemplate = `{
"produces": [
"application/json"
],
"tags": [
"add and modify"
],
"summary": "Marks job as completed and triggers archiving",
"parameters": [
{
@ -346,6 +568,9 @@ const docTemplate = `{
"produces": [
"application/json"
],
"tags": [
"add and modify"
],
"summary": "Adds one or more tags to a job",
"parameters": [
{
@ -363,7 +588,7 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/api.Tag"
"$ref": "#/definitions/api.ApiTag"
}
}
}
@ -404,8 +629,53 @@ const docTemplate = `{
}
},
"definitions": {
"api.ApiTag": {
"type": "object",
"properties": {
"name": {
"description": "Tag Name",
"type": "string",
"example": "Testjob"
},
"type": {
"description": "Tag Type",
"type": "string",
"example": "Debug"
}
}
},
"api.DeleteJobApiRequest": {
"type": "object",
"required": [
"jobId"
],
"properties": {
"cluster": {
"description": "Cluster of job",
"type": "string",
"example": "fritz"
},
"jobId": {
"description": "Cluster Job ID of job",
"type": "integer",
"example": 123000
},
"startTime": {
"description": "Start Time of job as epoch",
"type": "integer",
"example": 1649723812
}
}
},
"api.DeleteJobApiResponse": {
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
},
"api.ErrorResponse": {
"description": "Error message as returned from backend.",
"type": "object",
"properties": {
"error": {
@ -419,7 +689,6 @@ const docTemplate = `{
}
},
"api.StartJobApiResponse": {
"description": "Successful job start response with database id of new job.",
"type": "object",
"properties": {
"id": {
@ -429,7 +698,6 @@ const docTemplate = `{
}
},
"api.StopJobApiRequest": {
"description": "Request to stop running job using stoptime and final state. They are only required if no database id was provided with endpoint.",
"type": "object",
"required": [
"jobState",
@ -470,22 +738,6 @@ const docTemplate = `{
}
}
},
"api.Tag": {
"description": "Defines a tag using name and type.",
"type": "object",
"properties": {
"name": {
"description": "Tag Name",
"type": "string",
"example": "Testjob"
},
"type": {
"description": "Tag Type",
"type": "string",
"example": "Debug"
}
}
},
"schema.Job": {
"description": "Information of a HPC job.",
"type": "object",
@ -839,22 +1091,26 @@ const docTemplate = `{
},
"securityDefinitions": {
"ApiKeyAuth": {
"description": "JWT based authentification for general API endpoint use.",
"type": "apiKey",
"name": "X-Auth-Token",
"in": "header"
}
},
"tags": [
{
"name": "Job API"
}
]
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "0.1.0",
Host: "clustercockpit.localhost:8082",
Version: "0.2.0",
Host: "localhost:8080",
BasePath: "/api",
Schemes: []string{},
Title: "ClusterCockpit REST API",
Description: "Defines a tag using name and type.",
Description: "API for batch job control.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
}

View File

@ -32,9 +32,10 @@ import (
)
// @title ClusterCockpit REST API
// @version 0.1.0
// @version 0.2.0
// @description API for batch job control.
// @termsOfService https://monitoring.nhr.fau.de/imprint
// @tag.name Job API
// @contact.name ClusterCockpit Project
// @contact.url https://github.com/ClusterCockpit
@ -43,13 +44,12 @@ import (
// @license.name MIT License
// @license.url https://opensource.org/licenses/MIT
// @host clustercockpit.localhost:8082
// @BasePath /api
// @host localhost:8080
// @basePath /api
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name X-Auth-Token
// @description JWT based authentification for general API endpoint use.
type RestApi struct {
JobRepository *repository.JobRepository
@ -73,6 +73,9 @@ func (api *RestApi) MountRoutes(r *mux.Router) {
// r.HandleFunc("/jobs/{id}", api.getJob).Methods(http.MethodGet)
r.HandleFunc("/jobs/tag_job/{id}", api.tagJob).Methods(http.MethodPost, http.MethodPatch)
r.HandleFunc("/jobs/metrics/{id}", api.getJobMetrics).Methods(http.MethodGet)
r.HandleFunc("/jobs/delete_job/", api.deleteJobByRequest).Methods(http.MethodDelete)
r.HandleFunc("/jobs/delete_job/{id}", api.deleteJobById).Methods(http.MethodDelete)
r.HandleFunc("/jobs/delete_job_before/{ts}", api.deleteJobBefore).Methods(http.MethodDelete)
if api.Authentication != nil {
r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
@ -90,15 +93,17 @@ func (api *RestApi) MountRoutes(r *mux.Router) {
}
// StartJobApiResponse model
// @Description Successful job start response with database id of new job.
type StartJobApiResponse struct {
// Database ID of new job
DBID int64 `json:"id"`
}
// DeleteJobApiResponse model
type DeleteJobApiResponse struct {
Message string `json:"msg"`
}
// StopJobApiRequest model
// @Description Request to stop running job using stoptime and final state.
// @Description They are only required if no database id was provided with endpoint.
type StopJobApiRequest struct {
// Stop Time of job as epoch
StopTime int64 `json:"stopTime" validate:"required" example:"1649763839"`
@ -108,23 +113,28 @@ type StopJobApiRequest struct {
StartTime *int64 `json:"startTime" example:"1649723812"` // Start Time of job as epoch
}
// DeleteJobApiRequest model
type DeleteJobApiRequest struct {
JobId *int64 `json:"jobId" validate:"required" example:"123000"` // Cluster Job ID of job
Cluster *string `json:"cluster" example:"fritz"` // Cluster of job
StartTime *int64 `json:"startTime" example:"1649723812"` // Start Time of job as epoch
}
// ErrorResponse model
// @Description Error message as returned from backend.
type ErrorResponse struct {
// Statustext of Errorcode
Status string `json:"status"`
Error string `json:"error"` // Error Message
}
// Tag model
// @Description Defines a tag using name and type.
type Tag struct {
// ApiTag model
type ApiTag struct {
// Tag Type
Type string `json:"type" example:"Debug"`
Name string `json:"name" example:"Testjob"` // Tag Name
}
type TagJobApiRequest []*Tag
type TagJobApiRequest []*ApiTag
func handleError(err error, statusCode int, rw http.ResponseWriter) {
log.Warnf("REST API: %s", err.Error())
@ -143,28 +153,34 @@ func decode(r io.Reader, val interface{}) error {
}
// getJobs godoc
// @Summary Lists all jobs
// @Description Get a list of all jobs. Filters can be applied using query parameters.
// @Description Number of results can be limited by page. Results are sorted by descending startTime.
// @Accept json
// @Produce json
// @Param state query string false "Job State" Enums(running, completed, failed, cancelled, stopped, timeout)
// @Param cluster query string false "Job Cluster"
// @Param start-time query string false "Syntax: '$from-$to', as unix epoch timestamps in seconds"
// @Param items-per-page query int false "Items per page (If empty: No Limit)"
// @Param page query int false "Page Number (If empty: No Paging)"
// @Param with-metadata query bool false "Include metadata (e.g. jobScript) in response"
// @Success 200 {array} schema.Job "Array of matching jobs"
// @Failure 400 {object} api.ErrorResponse "Bad Request"
// @Failure 401 {object} api.ErrorResponse "Unauthorized"
// @Failure 500 {object} api.ErrorResponse "Internal Server Error"
// @Security ApiKeyAuth
// @Router /jobs/ [get]
// @summary Lists all jobs
// @tags query
// @description Get a list of all jobs. Filters can be applied using query parameters.
// @description Number of results can be limited by page. Results are sorted by descending startTime.
// @produce json
// @param state query string false "Job State" Enums(running, completed, failed, cancelled, stopped, timeout)
// @param cluster query string false "Job Cluster"
// @param start-time query string false "Syntax: '$from-$to', as unix epoch timestamps in seconds"
// @param items-per-page query int false "Items per page (Default: 25)"
// @param page query int false "Page Number (Default: 1)"
// @param with-metadata query bool false "Include metadata (e.g. jobScript) in response"
// @success 200 {array} schema.Job "Array of matching jobs"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/ [get]
func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
return
}
withMetadata := false
filter := &model.JobFilter{}
page := &model.PageRequest{ItemsPerPage: -1, Page: 1}
page := &model.PageRequest{ItemsPerPage: 25, Page: 1}
order := &model.OrderByInput{Field: "startTime", Order: model.SortDirectionEnumDesc}
for key, vals := range r.URL.Query() {
switch key {
case "state":
@ -270,21 +286,27 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
}
// tagJob godoc
// @Summary Adds one or more tags to a job
// @Description Adds tag(s) to a job specified by DB ID. Name and Type of Tag(s) can be chosen freely.
// @Description If tagged job is already finished: Tag will be written directly to respective archive files.
// @Accept json
// @Produce json
// @Param id path int true "Job Database ID"
// @Param request body api.TagJobApiRequest true "Array of tag-objects to add"
// @Success 200 {object} schema.Job "Updated job resource"
// @Failure 400 {object} api.ErrorResponse "Bad Request"
// @Failure 401 {object} api.ErrorResponse "Unauthorized"
// @Failure 404 {object} api.ErrorResponse "Job or tag does not exist"
// @Failure 500 {object} api.ErrorResponse "Internal Server Error"
// @Security ApiKeyAuth
// @Router /jobs/tag_job/{id} [post]
// @summary Adds one or more tags to a job
// @tags add and modify
// @description Adds tag(s) to a job specified by DB ID. Name and Type of Tag(s) can be chosen freely.
// @description If tagged job is already finished: Tag will be written directly to respective archive files.
// @accept json
// @produce json
// @param id path int true "Job Database ID"
// @param request body api.TagJobApiRequest true "Array of tag-objects to add"
// @success 200 {object} schema.Job "Updated job resource"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 404 {object} api.ErrorResponse "Job or tag does not exist"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/tag_job/{id} [post]
func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
return
}
iid, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
@ -329,20 +351,21 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
}
// startJob godoc
// @Summary Adds a new job as "running"
// @Description Job specified in request body will be saved to database as "running" with new DB ID.
// @Description Job specifications follow the 'JobMeta' scheme, API will fail to execute if requirements are not met.
// @Accept json
// @Produce json
// @Param request body schema.JobMeta true "Job to add"
// @Success 201 {object} api.StartJobApiResponse "Job added successfully"
// @Failure 400 {object} api.ErrorResponse "Bad Request"
// @Failure 401 {object} api.ErrorResponse "Unauthorized"
// @Failure 403 {object} api.ErrorResponse "Forbidden"
// @Failure 422 {object} api.ErrorResponse "Unprocessable Entity: The combination of jobId, clusterId and startTime does already exist"
// @Failure 500 {object} api.ErrorResponse "Internal Server Error"
// @Security ApiKeyAuth
// @Router /jobs/start_job/ [post]
// @summary Adds a new job as "running"
// @tags add and modify
// @description Job specified in request body will be saved to database as "running" with new DB ID.
// @description Job specifications follow the 'JobMeta' scheme, API will fail to execute if requirements are not met.
// @accept json
// @produce json
// @param request body schema.JobMeta true "Job to add"
// @success 201 {object} api.StartJobApiResponse "Job added successfully"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: The combination of jobId, clusterId and startTime does already exist"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/start_job/ [post]
func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
@ -407,22 +430,23 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
}
// stopJobById godoc
// @Summary Marks job as completed and triggers archiving
// @Description Job to stop is specified by database ID. Only stopTime and final state are required in request body.
// @Description Returns full job resource information according to 'JobMeta' scheme.
// @Accept json
// @Produce json
// @Param id path int true "Database ID of Job"
// @Param request body api.StopJobApiRequest true "stopTime and final state in request body"
// @Success 200 {object} schema.JobMeta "Job resource"
// @Failure 400 {object} api.ErrorResponse "Bad Request"
// @Failure 401 {object} api.ErrorResponse "Unauthorized"
// @Failure 403 {object} api.ErrorResponse "Forbidden"
// @Failure 404 {object} api.ErrorResponse "Resource not found"
// @Failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @Failure 500 {object} api.ErrorResponse "Internal Server Error"
// @Security ApiKeyAuth
// @Router /jobs/stop_job/{id} [post]
// @summary Marks job as completed and triggers archiving
// @tags add and modify
// @description Job to stop is specified by database ID. Only stopTime and final state are required in request body.
// @description Returns full job resource information according to 'JobMeta' scheme.
// @accept json
// @produce json
// @param id path int true "Database ID of Job"
// @param request body api.StopJobApiRequest true "stopTime and final state in request body"
// @success 200 {object} schema.JobMeta "Job resource"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/stop_job/{id} [post]
func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
@ -461,20 +485,21 @@ func (api *RestApi) stopJobById(rw http.ResponseWriter, r *http.Request) {
}
// stopJobByRequest godoc
// @Summary Marks job as completed and triggers archiving
// @Description Job to stop is specified by request body. All fields are required in this case.
// @Description Returns full job resource information according to 'JobMeta' scheme.
// @Produce json
// @Param request body api.StopJobApiRequest true "All fields required"
// @Success 200 {object} schema.JobMeta "Job resource"
// @Failure 400 {object} api.ErrorResponse "Bad Request"
// @Failure 401 {object} api.ErrorResponse "Unauthorized"
// @Failure 403 {object} api.ErrorResponse "Forbidden"
// @Failure 404 {object} api.ErrorResponse "Resource not found"
// @Failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @Failure 500 {object} api.ErrorResponse "Internal Server Error"
// @Security ApiKeyAuth
// @Router /jobs/stop_job/ [post]
// @summary Marks job as completed and triggers archiving
// @tags add and modify
// @description Job to stop is specified by request body. All fields are required in this case.
// @description Returns full job resource information according to 'JobMeta' scheme.
// @produce json
// @param request body api.StopJobApiRequest true "All fields required"
// @success 200 {object} schema.JobMeta "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/stop_job/ [post]
func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
@ -506,6 +531,159 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
api.checkAndHandleStopJob(rw, job, req)
}
// deleteJobById godoc
// @summary Remove a job from the sql database
// @tags remove
// @description Job to remove is specified by database ID. This will not remove the job from the job archive.
// @produce json
// @param id path int true "Database ID of Job"
// @success 200 {object} api.DeleteJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/delete_job/{id} [delete]
func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
return
}
// Fetch job (that will be stopped) from db
id, ok := mux.Vars(r)["id"]
var err error
if ok {
id, e := strconv.ParseInt(id, 10, 64)
if e != nil {
handleError(fmt.Errorf("integer expected in path for id: %w", e), http.StatusBadRequest, rw)
return
}
err = api.JobRepository.DeleteJobById(id)
} else {
handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw)
return
}
if err != nil {
handleError(fmt.Errorf("deleting job failed: %w", err), http.StatusUnprocessableEntity, rw)
return
}
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(DeleteJobApiResponse{
Message: fmt.Sprintf("Successfully deleted job %s", id),
})
}
// deleteJobByRequest godoc
// @summary Remove a job from the sql database
// @tags remove
// @description Job to delete is specified by request body. All fields are required in this case.
// @accept json
// @produce json
// @param request body api.DeleteJobApiRequest true "All fields required"
// @success 200 {object} api.DeleteJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/delete_job/ [delete]
func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
return
}
// Parse request body
req := DeleteJobApiRequest{}
if err := decode(r.Body, &req); err != nil {
handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
return
}
// Fetch job (that will be deleted) from db
var job *schema.Job
var err error
if req.JobId == nil {
handleError(errors.New("the field 'jobId' is required"), http.StatusBadRequest, rw)
return
}
job, err = api.JobRepository.Find(req.JobId, req.Cluster, req.StartTime)
if err != nil {
handleError(fmt.Errorf("finding job failed: %w", err), http.StatusUnprocessableEntity, rw)
return
}
err = api.JobRepository.DeleteJobById(job.ID)
if err != nil {
handleError(fmt.Errorf("deleting job failed: %w", err), http.StatusUnprocessableEntity, rw)
return
}
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(DeleteJobApiResponse{
Message: fmt.Sprintf("Successfully deleted job %d", job.ID),
})
}
// deleteJobBefore godoc
// @summary Remove a job from the sql database
// @tags remove
// @description Remove all jobs with start time before timestamp. The jobs will not be removed from the job archive.
// @produce json
// @param ts path int true "Unix epoch timestamp"
// @success 200 {object} api.DeleteJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/delete_job_before/{ts} [delete]
func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %#v", auth.RoleApi), http.StatusForbidden, rw)
return
}
var cnt int
// Fetch job (that will be stopped) from db
id, ok := mux.Vars(r)["ts"]
var err error
if ok {
ts, e := strconv.ParseInt(id, 10, 64)
if e != nil {
handleError(fmt.Errorf("integer expected in path for ts: %w", e), http.StatusBadRequest, rw)
return
}
cnt, err = api.JobRepository.DeleteJobsBefore(ts)
} else {
handleError(errors.New("the parameter 'ts' is required"), http.StatusBadRequest, rw)
return
}
if err != nil {
handleError(fmt.Errorf("deleting jobs failed: %w", err), http.StatusUnprocessableEntity, rw)
return
}
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
json.NewEncoder(rw).Encode(DeleteJobApiResponse{
Message: fmt.Sprintf("Successfully deleted %d jobs", cnt),
})
}
func (api *RestApi) checkAndHandleStopJob(rw http.ResponseWriter, job *schema.Job, req StopJobApiRequest) {
// Sanity checks

View File

@ -246,6 +246,29 @@ func (r *JobRepository) Stop(
return
}
func (r *JobRepository) DeleteJobsBefore(startTime int64) (int, error) {
var cnt int
qs := fmt.Sprintf("SELECT count(*) FROM job WHERE job.start_time < %d", startTime)
err := r.DB.Get(&cnt, qs) //ignore error as it will also occur in delete statement
_, err = r.DB.Exec(`DELETE FROM job WHERE job.start_time < ?`, startTime)
if err != nil {
log.Warnf(" DeleteJobsBefore(%d): error %v", startTime, err)
} else {
log.Infof("DeleteJobsBefore(%d): Deleted %d jobs", startTime, cnt)
}
return cnt, err
}
func (r *JobRepository) DeleteJobById(id int64) error {
_, err := r.DB.Exec(`DELETE FROM job WHERE job.id = ?`, id)
if err != nil {
log.Warnf("DeleteJobById(%d): error %v", id, err)
} else {
log.Infof("DeleteJobById(%d): Success", id)
}
return err
}
// TODO: Use node hours instead: SELECT job.user, sum(job.num_nodes * (CASE WHEN job.job_state = "running" THEN CAST(strftime('%s', 'now') AS INTEGER) - job.start_time ELSE job.duration END)) as x FROM job GROUP BY user ORDER BY x DESC;
func (r *JobRepository) CountGroupedJobs(ctx context.Context, aggreg model.Aggregate, filters []*model.JobFilter, weight *model.Weights, limit *int) (map[string]int, error) {
if !aggreg.IsValid() {