diff --git a/cmd/cc-backend/main.go b/cmd/cc-backend/main.go index eb44313..7ad5382 100644 --- a/cmd/cc-backend/main.go +++ b/cmd/cc-backend/main.go @@ -40,10 +40,12 @@ import ( "github.com/google/gops/agent" "github.com/gorilla/handlers" "github.com/gorilla/mux" + "github.com/swaggo/http-swagger" _ "github.com/go-sql-driver/mysql" _ "github.com/mattn/go-sqlite3" _ "github.com/santhosh-tekuri/jsonschema/v5/httploader" + _ "github.com/ClusterCockpit/cc-backend/docs" ) func main() { @@ -51,7 +53,7 @@ func main() { var flagNewUser, flagDelUser, flagGenJWT, flagConfigFile, flagImportJob string flag.BoolVar(&flagReinitDB, "init-db", false, "Go through job-archive and re-initialize the 'job', 'tag', and 'jobtag' tables (all running jobs will be lost!)") flag.BoolVar(&flagSyncLDAP, "sync-ldap", false, "Sync the 'user' table with ldap") - flag.BoolVar(&flagServer, "server", false, "Do not start a server, stop right after initialization and argument handling") + flag.BoolVar(&flagServer, "server", false, "Start a server, continues listening on port after initialization and argument handling") flag.BoolVar(&flagGops, "gops", false, "Listen via github.com/google/gops/agent (for debugging)") flag.BoolVar(&flagDev, "dev", false, "Enable development components: GraphQL Playground and Swagger UI") flag.StringVar(&flagConfigFile, "config", "./config.json", "Overwrite the global config options by those specified in `config.json`") @@ -262,6 +264,7 @@ func main() { if flagDev { r.Handle("/playground", playground.Handler("GraphQL playground", "/query")) + secured.PathPrefix("/docs").Handler(httpSwagger.WrapHandler) } secured.Handle("/query", graphQLEndpoint) diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..8bc1e9c --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,552 @@ +// Package docs GENERATED BY SWAG; DO NOT EDIT +// This file was generated by swaggo/swag at +// 2022-09-15 11:56:55.737680755 +0200 CEST m=+0.135609460 +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "TODO", + "contact": { + "name": "HPC-Support", + "url": "TODO", + "email": "hpc-support@fau.de" + }, + "license": { + "name": "MIT License", + "url": "https://opensource.org/licenses/MIT" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/jobs/": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get a list of all jobs. Filters can be applied using query parameters.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "jobs" + ], + "summary": "List all jobs", + "parameters": [ + { + "enum": [ + "running", + "completed", + "failed", + "canceled", + "stopped", + "timeout" + ], + "type": "string", + "description": "Job State", + "name": "state", + "in": "query" + }, + { + "type": "string", + "description": "Job Cluster", + "name": "cluster", + "in": "query" + }, + { + "type": "string", + "description": "Syntax: \u003cfrom\u003e-\u003cto\u003e, where \u003cfrom\u003e and \u003cto\u003e are unix timestamps in seconds", + "name": "start-time", + "in": "query" + }, + { + "type": "integer", + "description": "Page Number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Items per page", + "name": "items-per-page", + "in": "query" + }, + { + "type": "boolean", + "description": "Include metadata in response", + "name": "with-metadata", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Array of jobs", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Job" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + } + } + } + }, + "/jobs/start_job/": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "A new job started. The body should be in the ` + "`" + `meta.json` + "`" + ` format\nbut some fields required there are optional here (e.g. ` + "`" + `jobState` + "`" + ` defaults to \"running\").", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "jobs" + ], + "summary": "Add a newly started job", + "parameters": [ + { + "description": "Job to add", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.Job" + } + } + ], + "responses": { + "201": { + "description": "Job added successfully", + "schema": { + "$ref": "#/definitions/api.StartJobApiResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, + "422": { + "description": "The combination of jobId, clusterId and startTime does already exist", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + } + } + } + }, + "/jobs/stop_job/": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Job to stop is specified by request body.\nAll fields are required in request body.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "jobs" + ], + "summary": "Mark job as stopped and trigger archiving", + "parameters": [ + { + "description": "All fields required", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.StopJobApiRequest" + } + } + ], + "responses": { + "201": { + "description": "Job resource", + "schema": { + "$ref": "#/definitions/schema.Job" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + } + } + } + }, + "/jobs/stop_job/{id}": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Job to stop is specified by database ID.\nOnly stopTime and final state are required in request body.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "jobs" + ], + "summary": "Mark job as stopped and trigger archiving", + "parameters": [ + { + "type": "integer", + "description": "Database ID of Job", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Required fields: [stopTime, state]", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.StopJobApiRequest" + } + } + ], + "responses": { + "201": { + "description": "Job resource", + "schema": { + "$ref": "#/definitions/schema.Job" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + } + } + } + }, + "/jobs/tag_job/{id}": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Add one or more tags as array in request body to job specified by DB ID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "jobs" + ], + "summary": "Add one or more tags to a job", + "parameters": [ + { + "type": "integer", + "description": "Job Database ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Array of tag-objects to add", + "name": "request", + "in": "body", + "required": true, + "schema": { + "description": "Array of tag-objects for request payload", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "description": "Tag Name", + "type": "string" + }, + "type": { + "description": "Tag Type", + "type": "string" + } + } + } + } + } + ], + "responses": { + "200": { + "description": "Job resource", + "schema": { + "$ref": "#/definitions/schema.Job" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, + "404": { + "description": "Job or tag does not exist", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "api.ErrorResponse": { + "description": "Error Response when using API.", + "type": "object", + "properties": { + "error": { + "description": "Error Message", + "type": "string" + }, + "status": { + "description": "Statustext of Errorcode", + "type": "string" + } + } + }, + "api.StartJobApiResponse": { + "description": "Successful job start response with database id of new job.", + "type": "object", + "properties": { + "id": { + "description": "Database ID of new job", + "type": "integer" + } + } + }, + "api.StopJobApiRequest": { + "description": "Request to stop running job using stop time and state. Optional fields: JobId, ClusterId and StartTime. They are only used if no database id was provided.", + "type": "object", + "properties": { + "cluster": { + "description": "Cluster of job (Optional)", + "type": "string" + }, + "jobId": { + "description": "Job ID of job (Optional)", + "type": "integer" + }, + "jobState": { + "description": "Final job state", + "type": "string" + }, + "startTime": { + "description": "Start Time of job (Optional)", + "type": "integer" + }, + "stopTime": { + "description": "Stop Time as Epoch", + "type": "integer" + } + } + }, + "api.TagJobApiRequest": { + "description": "Request to tag a job.", + "type": "array", + "items": { + "type": "object", + "properties": { + "error": { + "description": "Error Message", + "type": "string" + }, + "status": { + "description": "Statustext of Errorcode", + "type": "string" + } + } + } + }, + "schema.Job": { + "type": "object", + "properties": { + "arrayJobId": { + "type": "integer" + }, + "cluster": { + "type": "string" + }, + "duration": { + "type": "integer" + }, + "exclusive": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "jobId": { + "type": "integer" + }, + "jobState": { + "type": "string" + }, + "metaData": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "monitoringStatus": { + "type": "integer" + }, + "numAcc": { + "type": "integer" + }, + "numHwthreads": { + "type": "integer" + }, + "numNodes": { + "type": "integer" + }, + "partition": { + "type": "string" + }, + "project": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Resource" + } + }, + "smt": { + "type": "integer" + }, + "startTime": { + "type": "string" + }, + "subCluster": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Tag" + } + }, + "user": { + "type": "string" + }, + "walltime": { + "type": "integer" + } + } + }, + "schema.Resource": { + "type": "object", + "properties": { + "accelerators": { + "type": "array", + "items": { + "type": "string" + } + }, + "configuration": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "hwthreads": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "schema.Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "description": "JWT based authentification for general API endpoint use.", + "type": "apiKey", + "name": "X-Auth-Token", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "0.1.0", + Host: "clustercockpit.localhost:8082", + BasePath: "/api", + Schemes: []string{}, + Title: "ClusterCockpit REST API", + Description: "API for batch job control.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..7e1d92b --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,528 @@ +{ + "swagger": "2.0", + "info": { + "description": "Array of tag-objects for request payload", + "title": "ClusterCockpit REST API", + "termsOfService": "TODO", + "contact": { + "name": "HPC-Support", + "url": "TODO", + "email": "hpc-support@fau.de" + }, + "license": { + "name": "MIT License", + "url": "https://opensource.org/licenses/MIT" + }, + "version": "0.1.0" + }, + "host": "clustercockpit.localhost:8082", + "basePath": "/api", + "paths": { + "/jobs/": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get a list of all jobs. Filters can be applied using query parameters.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "jobs" + ], + "summary": "List all jobs", + "parameters": [ + { + "enum": [ + "running", + "completed", + "failed", + "canceled", + "stopped", + "timeout" + ], + "type": "string", + "description": "Job State", + "name": "state", + "in": "query" + }, + { + "type": "string", + "description": "Job Cluster", + "name": "cluster", + "in": "query" + }, + { + "type": "string", + "description": "Syntax: \u003cfrom\u003e-\u003cto\u003e, where \u003cfrom\u003e and \u003cto\u003e are unix timestamps in seconds", + "name": "start-time", + "in": "query" + }, + { + "type": "integer", + "description": "Page Number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Items per page", + "name": "items-per-page", + "in": "query" + }, + { + "type": "boolean", + "description": "Include metadata in response", + "name": "with-metadata", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Array of jobs", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Job" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + } + } + } + }, + "/jobs/start_job/": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "A new job started. The body should be in the `meta.json` format\nbut some fields required there are optional here (e.g. `jobState` defaults to \"running\").", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "jobs" + ], + "summary": "Add a newly started job", + "parameters": [ + { + "description": "Job to add", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.Job" + } + } + ], + "responses": { + "201": { + "description": "Job added successfully", + "schema": { + "$ref": "#/definitions/api.StartJobApiResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, + "422": { + "description": "The combination of jobId, clusterId and startTime does already exist", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + } + } + } + }, + "/jobs/stop_job/": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Job to stop is specified by request body.\nAll fields are required in request body.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "jobs" + ], + "summary": "Mark job as stopped and trigger archiving", + "parameters": [ + { + "description": "All fields required", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.StopJobApiRequest" + } + } + ], + "responses": { + "201": { + "description": "Job resource", + "schema": { + "$ref": "#/definitions/schema.Job" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + } + } + } + }, + "/jobs/stop_job/{id}": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Job to stop is specified by database ID.\nOnly stopTime and final state are required in request body.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "jobs" + ], + "summary": "Mark job as stopped and trigger archiving", + "parameters": [ + { + "type": "integer", + "description": "Database ID of Job", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Required fields: [stopTime, state]", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.StopJobApiRequest" + } + } + ], + "responses": { + "201": { + "description": "Job resource", + "schema": { + "$ref": "#/definitions/schema.Job" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + } + } + } + }, + "/jobs/tag_job/{id}": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Add one or more tags as array in request body to job specified by DB ID.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "jobs" + ], + "summary": "Add one or more tags to a job", + "parameters": [ + { + "type": "integer", + "description": "Job Database ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Array of tag-objects to add", + "name": "request", + "in": "body", + "required": true, + "schema": { + "description": "Array of tag-objects for request payload", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "description": "Tag Name", + "type": "string" + }, + "type": { + "description": "Tag Type", + "type": "string" + } + } + } + } + } + ], + "responses": { + "200": { + "description": "Job resource", + "schema": { + "$ref": "#/definitions/schema.Job" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + }, + "404": { + "description": "Job or tag does not exist", + "schema": { + "$ref": "#/definitions/api.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "api.ErrorResponse": { + "description": "Error Response when using API.", + "type": "object", + "properties": { + "error": { + "description": "Error Message", + "type": "string" + }, + "status": { + "description": "Statustext of Errorcode", + "type": "string" + } + } + }, + "api.StartJobApiResponse": { + "description": "Successful job start response with database id of new job.", + "type": "object", + "properties": { + "id": { + "description": "Database ID of new job", + "type": "integer" + } + } + }, + "api.StopJobApiRequest": { + "description": "Request to stop running job using stop time and state. Optional fields: JobId, ClusterId and StartTime. They are only used if no database id was provided.", + "type": "object", + "properties": { + "cluster": { + "description": "Cluster of job (Optional)", + "type": "string" + }, + "jobId": { + "description": "Job ID of job (Optional)", + "type": "integer" + }, + "jobState": { + "description": "Final job state", + "type": "string" + }, + "startTime": { + "description": "Start Time of job (Optional)", + "type": "integer" + }, + "stopTime": { + "description": "Stop Time as Epoch", + "type": "integer" + } + } + }, + "api.TagJobApiRequest": { + "description": "Request to tag a job.", + "type": "array", + "items": { + "type": "object", + "properties": { + "error": { + "description": "Error Message", + "type": "string" + }, + "status": { + "description": "Statustext of Errorcode", + "type": "string" + } + } + } + }, + "schema.Job": { + "type": "object", + "properties": { + "arrayJobId": { + "type": "integer" + }, + "cluster": { + "type": "string" + }, + "duration": { + "type": "integer" + }, + "exclusive": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "jobId": { + "type": "integer" + }, + "jobState": { + "type": "string" + }, + "metaData": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "monitoringStatus": { + "type": "integer" + }, + "numAcc": { + "type": "integer" + }, + "numHwthreads": { + "type": "integer" + }, + "numNodes": { + "type": "integer" + }, + "partition": { + "type": "string" + }, + "project": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Resource" + } + }, + "smt": { + "type": "integer" + }, + "startTime": { + "type": "string" + }, + "subCluster": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.Tag" + } + }, + "user": { + "type": "string" + }, + "walltime": { + "type": "integer" + } + } + }, + "schema.Resource": { + "type": "object", + "properties": { + "accelerators": { + "type": "array", + "items": { + "type": "string" + } + }, + "configuration": { + "type": "string" + }, + "hostname": { + "type": "string" + }, + "hwthreads": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "schema.Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "description": "JWT based authentification for general API endpoint use.", + "type": "apiKey", + "name": "X-Auth-Token", + "in": "header" + } + } +} diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..cba0d09 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,360 @@ +basePath: /api +definitions: + api.ErrorResponse: + description: Error Response when using API. + properties: + error: + description: Error Message + type: string + status: + description: Statustext of Errorcode + type: string + type: object + api.TagJobApiRequest: + description: Request to tag a job boii + items: + properties: + name: + description: Tag Name + type: string + type: + description: Tag Type + type: string + type: object + type: array + 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 stop time and state. Optional + fields: JobId, ClusterId and StartTime. They are only used if no database id + was provided.' + properties: + cluster: + description: Cluster of job (Optional) + type: string + jobId: + description: Job ID of job (Optional) + type: integer + jobState: + description: Final job state + type: string + startTime: + description: Start Time of job (Optional) + type: integer + stopTime: + description: Stop Time as Epoch + type: integer + type: object + schema.Job: + properties: + arrayJobId: + type: integer + cluster: + type: string + duration: + type: integer + exclusive: + type: integer + id: + type: integer + jobId: + type: integer + jobState: + type: string + metaData: + additionalProperties: + type: string + type: object + monitoringStatus: + type: integer + numAcc: + type: integer + numHwthreads: + type: integer + numNodes: + type: integer + partition: + type: string + project: + type: string + resources: + items: + $ref: '#/definitions/schema.Resource' + type: array + smt: + type: integer + startTime: + type: string + subCluster: + type: string + tags: + items: + $ref: '#/definitions/schema.Tag' + type: array + user: + type: string + walltime: + type: integer + type: object + schema.Resource: + properties: + accelerators: + items: + type: string + type: array + configuration: + type: string + hostname: + type: string + hwthreads: + items: + type: integer + type: array + type: object + schema.Tag: + properties: + id: + type: integer + name: + type: string + type: + type: string + type: object +host: clustercockpit.localhost:8082 +info: + contact: + email: hpc-support@fau.de + name: HPC-Support + url: TODO + description: Array of tag-objects for request payload + license: + name: MIT License + url: https://opensource.org/licenses/MIT + termsOfService: TODO + title: ClusterCockpit REST API + version: 0.1.0 +paths: + /jobs/: + get: + consumes: + - application/json + description: Get a list of all jobs. Filters can be applied using query parameters. + parameters: + - description: Job State + enum: + - running + - completed + - failed + - canceled + - stopped + - timeout + in: query + name: state + type: string + - description: Job Cluster + in: query + name: cluster + type: string + - description: 'Syntax: -, where and are unix timestamps + in seconds' + in: query + name: start-time + type: string + - description: Page Number + in: query + name: page + type: integer + - description: Items per page + in: query + name: items-per-page + type: integer + - description: Include metadata in response + in: query + name: with-metadata + type: boolean + produces: + - application/json + responses: + "200": + description: Array of jobs + schema: + items: + $ref: '#/definitions/schema.Job' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: List all jobs + tags: + - jobs + /jobs/start_job/: + post: + consumes: + - application/json + description: |- + A new job started. The body should be in the `meta.json` format + but some fields required there are optional here (e.g. `jobState` defaults to "running"). + parameters: + - description: Job to add + in: body + name: request + required: true + schema: + $ref: '#/definitions/schema.Job' + produces: + - application/json + responses: + "201": + description: Job added successfully + schema: + $ref: '#/definitions/api.StartJobApiResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.ErrorResponse' + "422": + description: The combination of jobId, clusterId and startTime does already + exist + schema: + $ref: '#/definitions/api.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: Add a newly started job + tags: + - jobs + /jobs/stop_job/: + post: + consumes: + - application/json + description: |- + Job to stop is specified by request body. + All fields are required in request body. + parameters: + - description: All fields required + in: body + name: request + required: true + schema: + $ref: '#/definitions/api.StopJobApiRequest' + produces: + - application/json + responses: + "201": + description: Job resource + schema: + $ref: '#/definitions/schema.Job' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.ErrorResponse' + "404": + description: Resource not found + schema: + $ref: '#/definitions/api.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: Mark job as stopped and trigger archiving + tags: + - jobs + /jobs/stop_job/{id}: + post: + consumes: + - application/json + description: |- + Job to stop is specified by database ID. + Only stopTime and final state are required in request body. + parameters: + - description: Database ID of Job + in: path + name: id + required: true + type: integer + - description: 'Required fields: [stopTime, state]' + in: body + name: request + required: true + schema: + $ref: '#/definitions/api.StopJobApiRequest' + produces: + - application/json + responses: + "201": + description: Job resource + schema: + $ref: '#/definitions/schema.Job' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.ErrorResponse' + "404": + description: Resource not found + schema: + $ref: '#/definitions/api.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: Mark job as stopped and trigger archiving + tags: + - jobs + /jobs/tag_job/{id}: + post: + consumes: + - application/json + description: Add one or more tags as array in request body to job specified + by DB ID. + parameters: + - description: Job Database ID + in: path + name: id + required: true + type: integer + - description: Array of tag-objects to add + in: body + name: request + required: true + schema: + description: Array of tag-objects for request payload + items: + properties: + name: + description: Tag Name + type: string + type: + description: Tag Type + type: string + type: object + type: array + produces: + - application/json + responses: + "200": + description: Job resource + schema: + $ref: '#/definitions/schema.Job' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.ErrorResponse' + "404": + description: Job or tag does not exist + schema: + $ref: '#/definitions/api.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: Add one or more tags to a job + tags: + - jobs +securityDefinitions: + ApiKeyAuth: + description: JWT based authentification for general API endpoint use. + in: header + name: X-Auth-Token + type: apiKey +swagger: "2.0" diff --git a/go.mod b/go.mod index 1abcbd0..deef30c 100644 --- a/go.mod +++ b/go.mod @@ -22,29 +22,39 @@ require ( require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect + github.com/KyleBanks/depth v1.2.1 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/deepmap/oapi-codegen v1.11.0 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.7 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect + github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect + github.com/swaggo/http-swagger v1.3.3 // indirect + github.com/swaggo/swag v1.8.5 // indirect github.com/urfave/cli/v2 v2.8.1 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect - golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect - golang.org/x/sys v0.0.0-20220907062415-87db552b00fd // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect + golang.org/x/sys v0.0.0-20220913175220-63ea55921009 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.10 // indirect + golang.org/x/tools v0.1.12 // indirect golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index cd73855..a9937f5 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/99designs/gqlgen v0.17.16/go.mod h1:dnJdUkgfh8iw8CEx2hhTdgTQO/GvVWKLc github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU= github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= @@ -43,9 +45,18 @@ github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITL github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= +github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= @@ -91,6 +102,7 @@ github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmne github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -129,6 +141,7 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2 github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= @@ -181,6 +194,13 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY= +github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc= +github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo= +github.com/swaggo/swag v1.8.5 h1:7NgtfXsXE+jrcOwRyiftGKW7Ppydj7tZiVenuRf1fE4= +github.com/swaggo/swag v1.8.5/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= @@ -213,16 +233,21 @@ golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5 golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo= golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -252,6 +277,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220907062415-87db552b00fd h1:AZeIEzg+8RCELJYq8w+ODLVxFgLMMigSwO/ffKPEd9U= golang.org/x/sys v0.0.0-20220907062415-87db552b00fd/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220913175220-63ea55921009 h1:PuvuRMeLWqsf/ZdT1UUZz0syhioyv1mzuFZsXs4fvhw= +golang.org/x/sys v0.0.0-20220913175220-63ea55921009/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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= @@ -270,6 +297,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/api/rest.go b/internal/api/rest.go index 077a705..e42965a 100644 --- a/internal/api/rest.go +++ b/internal/api/rest.go @@ -31,6 +31,22 @@ import ( "github.com/gorilla/mux" ) +// @title ClusterCockpit REST API +// @version 0.1.0 +// @description API for batch job control. +// @termsOfService TODO +// @contact.name HPC-Support +// @contact.url TODO +// @contact.email hpc-support@fau.de +// @license.name MIT License +// @license.url https://opensource.org/licenses/MIT +// @host clustercockpit.localhost:8082 +// @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 Resolver *graph.Resolver @@ -44,8 +60,8 @@ func (api *RestApi) MountRoutes(r *mux.Router) { r.StrictSlash(true) r.HandleFunc("/jobs/start_job/", api.startJob).Methods(http.MethodPost, http.MethodPut) - r.HandleFunc("/jobs/stop_job/", api.stopJob).Methods(http.MethodPost, http.MethodPut) - r.HandleFunc("/jobs/stop_job/{id}", api.stopJob).Methods(http.MethodPost, http.MethodPut) + r.HandleFunc("/jobs/stop_job/", api.stopJobByRequest).Methods(http.MethodPost, http.MethodPut) + r.HandleFunc("/jobs/stop_job/{id}", api.stopJobById).Methods(http.MethodPost, http.MethodPut) // r.HandleFunc("/jobs/import/", api.importJob).Methods(http.MethodPost, http.MethodPut) r.HandleFunc("/jobs/", api.getJobs).Methods(http.MethodGet) @@ -68,25 +84,40 @@ 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"` } +// StopJobApiRequest model +// @Description Request to stop running job using stop time and state. +// @Description Optional fields: JobId, ClusterId and StartTime. +// @Description They are only used if no database id was provided. type StopJobApiRequest struct { - // JobId, ClusterId and StartTime are optional. - // They are only used if no database id was provided. - JobId *int64 `json:"jobId"` - Cluster *string `json:"cluster"` - StartTime *int64 `json:"startTime"` - - // Payload + // Stop Time as Epoch StopTime int64 `json:"stopTime"` - State schema.JobState `json:"jobState"` + State schema.JobState `json:"jobState"` // Final job state + JobId *int64 `json:"jobId"` // Job ID of job (Optional) + Cluster *string `json:"cluster"` // Cluster of job (Optional) + StartTime *int64 `json:"startTime"` // Start Time of job (Optional) } +// ErrorResponse model +// @Description Error Response when using API. type ErrorResponse struct { + // Statustext of Errorcode Status string `json:"status"` - Error string `json:"error"` + Error string `json:"error"` // Error Message +} + +// TagJobApiRequest model +// @Description Array of tag-objects for request payload +type TagJobApiRequest []*struct { + // Tag Name + Name string `json:"name"` + Type string `json:"type"` // Tag Type } func handleError(err error, statusCode int, rw http.ResponseWriter) { @@ -105,12 +136,24 @@ func decode(r io.Reader, val interface{}) error { return dec.Decode(val) } -type TagJobApiRequest []*struct { - Name string `json:"name"` - Type string `json:"type"` -} -// Return a list of jobs + +// getJobs godoc +// @Summary List all jobs +// @Description Get a list of all jobs. Filters can be applied using query parameters. +// @Tags jobs +// @Accept json +// @Produce json +// @Param state query string false "Job State" Enums(running, completed, failed, canceled, stopped, timeout) +// @Param cluster query string false "Job Cluster" +// @Param start-time query string false "Syntax: -, where and are unix timestamps in seconds" +// @Param page query int false "Page Number" +// @Param items-per-page query int false "Items per page" +// @Param with-metadata query bool false "Include metadata in response" +// @Success 200 {array} schema.Job "Array of jobs" +// @Failure 400 {object} api.ErrorResponse "Bad Request" +// @Security ApiKeyAuth +// @Router /jobs/ [get] func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) { withMetadata := false filter := &model.JobFilter{} @@ -220,7 +263,19 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) { } } -// Add a tag to a job +// tagJob godoc +// @Summary Add one or more tags to a job +// @Description Add one or more tags as array in request body to job specified by DB ID. +// @Tags jobs +// @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 "Job resource" +// @Failure 400 {object} api.ErrorResponse "Bad Request" +// @Failure 404 {object} api.ErrorResponse "Job or tag does not exist" +// @Security ApiKeyAuth +// @Router /jobs/tag_job/{id} [post] func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) { iid, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64) if err != nil { @@ -265,8 +320,19 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) { json.NewEncoder(rw).Encode(job) } -// A new job started. The body should be in the `meta.json` format, but some fields required -// there are optional here (e.g. `jobState` defaults to "running"). +// startJob godoc +// @Summary Add a newly started job +// @Description A new job started. The body should be in the `meta.json` format +// @Description but some fields required there are optional here (e.g. `jobState` defaults to "running"). +// @Tags jobs +// @Accept json +// @Produce json +// @Param request body schema.Job true "Job to add" +// @Success 201 {object} api.StartJobApiResponse "Job added successfully" +// @Failure 400 {object} api.ErrorResponse "Bad Request" +// @Failure 422 {object} api.ErrorResponse "The combination of jobId, clusterId and startTime does already exist" +// @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) @@ -321,14 +387,27 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) { }) } -// A job has stopped and should be archived. -func (api *RestApi) stopJob(rw http.ResponseWriter, r *http.Request) { +// stopJobById godoc +// @Summary Mark job as stopped and trigger archiving +// @Description Job to stop is specified by database ID. +// @Description Only stopTime and final state are required in request body. +// @Tags jobs +// @Accept json +// @Produce json +// @Param id path int true "Database ID of Job" +// @Param request body api.StopJobApiRequest true "Required fields: [stopTime, state]" +// @Success 201 {object} schema.Job "Job resource" +// @Failure 400 {object} api.ErrorResponse "Bad Request" +// @Failure 404 {object} api.ErrorResponse "Resource not found" +// @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) return } - // Parse request body + // Parse request body: Only StopTime and State req := StopJobApiRequest{} if err := decode(r.Body, &req); err != nil { handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw) @@ -348,13 +427,114 @@ func (api *RestApi) stopJob(rw http.ResponseWriter, r *http.Request) { job, err = api.JobRepository.FindById(id) } else { - if req.JobId == nil { - handleError(errors.New("the field 'jobId' is required"), http.StatusBadRequest, rw) + handleError(errors.New("the parameter 'id' is required"), http.StatusBadRequest, rw) + return + } + if err != nil { + handleError(fmt.Errorf("finding job failed: %w", err), http.StatusUnprocessableEntity, rw) + return + } + + // Sanity checks + if job == nil || job.StartTime.Unix() >= req.StopTime || job.State != schema.JobStateRunning { + handleError(errors.New("stopTime must be larger than startTime and only running jobs can be stopped"), http.StatusBadRequest, rw) + return + } + if req.State != "" && !req.State.Valid() { + handleError(fmt.Errorf("invalid job state: %#v", req.State), http.StatusBadRequest, rw) + return + } else { + req.State = schema.JobStateCompleted + } + + // Mark job as stopped in the database (update state and duration) + job.Duration = int32(req.StopTime - job.StartTime.Unix()) + job.State = req.State + if err := api.JobRepository.Stop(job.ID, job.Duration, job.State, job.MonitoringStatus); err != nil { + handleError(fmt.Errorf("marking job as stopped failed: %w", err), http.StatusInternalServerError, rw) + return + } + + log.Printf("archiving job... (dbid: %d): cluster=%s, jobId=%d, user=%s, startTime=%s", job.ID, job.Cluster, job.JobID, job.User, job.StartTime) + + // Send a response (with status OK). This means that erros that happen from here on forward + // can *NOT* be communicated to the client. If reading from a MetricDataRepository or + // writing to the filesystem fails, the client will not know. + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(http.StatusOK) + json.NewEncoder(rw).Encode(job) + + // Monitoring is disabled... + if job.MonitoringStatus == schema.MonitoringStatusDisabled { + return + } + + // We need to start a new goroutine as this functions needs to return + // for the response to be flushed to the client. + api.OngoingArchivings.Add(1) // So that a shutdown does not interrupt this goroutine. + go func() { + defer api.OngoingArchivings.Done() + + if _, err := api.JobRepository.FetchMetadata(job); err != nil { + log.Errorf("archiving job (dbid: %d) failed: %s", job.ID, err.Error()) + api.JobRepository.UpdateMonitoringStatus(job.ID, schema.MonitoringStatusArchivingFailed) return } - job, err = api.JobRepository.Find(req.JobId, req.Cluster, req.StartTime) + // metricdata.ArchiveJob will fetch all the data from a MetricDataRepository and create meta.json/data.json files + jobMeta, err := metricdata.ArchiveJob(job, context.Background()) + if err != nil { + log.Errorf("archiving job (dbid: %d) failed: %s", job.ID, err.Error()) + api.JobRepository.UpdateMonitoringStatus(job.ID, schema.MonitoringStatusArchivingFailed) + return + } + + // Update the jobs database entry one last time: + if err := api.JobRepository.Archive(job.ID, schema.MonitoringStatusArchivingSuccessful, jobMeta.Statistics); err != nil { + log.Errorf("archiving job (dbid: %d) failed: %s", job.ID, err.Error()) + return + } + + log.Printf("archiving job (dbid: %d) successful", job.ID) + }() +} + +// stopJobByRequest godoc +// @Summary Mark job as stopped and trigger archiving +// @Description Job to stop is specified by request body. +// @Description All fields are required in request body. +// @Tags jobs +// @Accept json +// @Produce json +// @Param request body api.StopJobApiRequest true "All fields required" +// @Success 201 {object} schema.Job "Job resource" +// @Failure 400 {object} api.ErrorResponse "Bad Request" +// @Failure 404 {object} api.ErrorResponse "Resource not found" +// @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) + return } + + // Parse request body + req := StopJobApiRequest{} + 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 stopped) 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 @@ -447,6 +627,7 @@ func (api *RestApi) stopJob(rw http.ResponseWriter, r *http.Request) { // rw.Write([]byte(`{ "status": "OK" }`)) // } + func (api *RestApi) getJobMetrics(rw http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] metrics := r.URL.Query()["metric"] @@ -489,6 +670,9 @@ func (api *RestApi) getJobMetrics(rw http.ResponseWriter, r *http.Request) { }) } + + + func (api *RestApi) getJWT(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "text/plain") username := r.FormValue("username") @@ -603,6 +787,10 @@ func (api *RestApi) updateConfiguration(rw http.ResponseWriter, r *http.Request) rw.Write([]byte("success")) } + + + + func (api *RestApi) putMachineState(rw http.ResponseWriter, r *http.Request) { if api.MachineStateDir == "" { http.Error(rw, "not enabled", http.StatusNotFound)