mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-04-29 22:51:42 +02:00
commit
ff588ad57a
@ -277,6 +277,7 @@ type Mutation {
|
||||
deleteTag(id: ID!): ID!
|
||||
addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]!
|
||||
removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]!
|
||||
removeTagFromList(tagIds: [ID!]!): [Int!]!
|
||||
|
||||
updateConfiguration(name: String!, value: String!): String
|
||||
}
|
||||
|
417
api/swagger.json
417
api/swagger.json
@ -15,9 +15,8 @@
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"host": "localhost:8080",
|
||||
"basePath": "/api",
|
||||
"paths": {
|
||||
"/clusters/": {
|
||||
"/api/clusters/": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
@ -74,7 +73,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/": {
|
||||
"/api/jobs/": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
@ -169,7 +168,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/delete_job/": {
|
||||
"/api/jobs/delete_job/": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
@ -244,7 +243,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/delete_job/{id}": {
|
||||
"/api/jobs/delete_job/{id}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
@ -314,7 +313,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/delete_job_before/{ts}": {
|
||||
"/api/jobs/delete_job_before/{ts}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
@ -384,7 +383,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/edit_meta/{id}": {
|
||||
"/api/jobs/edit_meta/{id}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
@ -454,7 +453,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/start_job/": {
|
||||
"/api/jobs/start_job/": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
@ -523,7 +522,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/stop_job/": {
|
||||
"/api/jobs/stop_job/": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
@ -595,7 +594,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/tag_job/{id}": {
|
||||
"/api/jobs/tag_job/{id}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
@ -668,7 +667,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/{id}": {
|
||||
"/api/jobs/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
@ -827,185 +826,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/notice/": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Modifies the content of notice.txt, shown as notice box on the homepage.\nIf more than one formValue is set then only the highest priority field is used.\nOnly accessible from IPs registered with apiAllowedIPs configuration option.",
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Updates or empties the notice box content",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Priority 1: New content to display",
|
||||
"name": "new-content",
|
||||
"in": "formData"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success Response Message",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity: The user could not be updated",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/{id}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Modifies user defined by username (id) in one of four possible ways.\nIf more than one formValue is set then only the highest priority field is used.\nOnly accessible from IPs registered with apiAllowedIPs configuration option.",
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Updates an existing user",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Database ID of User",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"admin",
|
||||
"support",
|
||||
"manager",
|
||||
"user",
|
||||
"api"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Priority 1: Role to add",
|
||||
"name": "add-role",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"admin",
|
||||
"support",
|
||||
"manager",
|
||||
"user",
|
||||
"api"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Priority 2: Role to remove",
|
||||
"name": "remove-role",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Priority 3: Project to add",
|
||||
"name": "add-project",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Priority 4: Project to remove",
|
||||
"name": "remove-project",
|
||||
"in": "formData"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success Response Message",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity: The user could not be updated",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/": {
|
||||
"/api/users/": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a JSON-encoded list of users.\nRequired query-parameter defines if all users or only users with additional special roles are returned.\nOnly accessible from IPs registered with apiAllowedIPs configuration option.",
|
||||
"description": "Returns a JSON-encoded list of users.\nRequired query-parameter defines if all users or only users with additional special roles are returned.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -1057,70 +885,111 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
}
|
||||
},
|
||||
"/jobs/tag_job/{id}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "User specified in form data will be saved to database.\nOnly accessible from IPs registered with apiAllowedIPs configuration option.",
|
||||
"description": "Removes tag(s) from a job specified by DB ID. Name and Type of Tag(s) must match.\nTag Scope is required for matching, options: \"global\", \"admin\". Private tags can not be deleted via API.\nIf tagged job is already finished: Tag will be removed from respective archive files.",
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Job add and modify"
|
||||
],
|
||||
"summary": "Removes one or more tags from a job",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Job Database ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Array of tag-objects to remove",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.ApiTag"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Updated job resource",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.Job"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Job or tag does not exist",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tags/": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Removes tags by type and name. Name and Type of Tag(s) must match.\nTag Scope is required for matching, options: \"global\", \"admin\". Private tags can not be deleted via API.\nTag wills be removed from respective archive files.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
"Tag remove"
|
||||
],
|
||||
"summary": "Adds a new user",
|
||||
"summary": "Removes all tags and job-relations for type:name tuple",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Unique user ID",
|
||||
"name": "username",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User password",
|
||||
"name": "password",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"admin",
|
||||
"support",
|
||||
"manager",
|
||||
"user",
|
||||
"api"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "User role",
|
||||
"name": "role",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Managed project, required for new manager role user",
|
||||
"name": "project",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Users name",
|
||||
"name": "name",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Users email",
|
||||
"name": "email",
|
||||
"in": "formData"
|
||||
"description": "Array of tag-objects to remove",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.ApiTag"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@ -1133,93 +1002,25 @@
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"404": {
|
||||
"description": "Job or tag does not exist",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity: creating user failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "User defined by username in form data will be deleted from database.\nOnly accessible from IPs registered with apiAllowedIPs configuration option.",
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Deletes a user",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID to delete",
|
||||
"name": "username",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "User deleted successfully"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity: deleting user failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
300
api/swagger.yaml
300
api/swagger.yaml
@ -1,4 +1,3 @@
|
||||
basePath: /api
|
||||
definitions:
|
||||
api.ApiReturnedUser:
|
||||
properties:
|
||||
@ -671,7 +670,7 @@ info:
|
||||
title: ClusterCockpit REST API
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/clusters/:
|
||||
/api/clusters/:
|
||||
get:
|
||||
description: Get a list of all cluster configs. Specific cluster can be requested
|
||||
using query parameter.
|
||||
@ -708,7 +707,7 @@ paths:
|
||||
summary: Lists all cluster configs
|
||||
tags:
|
||||
- Cluster query
|
||||
/jobs/:
|
||||
/api/jobs/:
|
||||
get:
|
||||
description: |-
|
||||
Get a list of all jobs. Filters can be applied using query parameters.
|
||||
@ -773,7 +772,7 @@ paths:
|
||||
summary: Lists all jobs
|
||||
tags:
|
||||
- Job query
|
||||
/jobs/{id}:
|
||||
/api/jobs/{id}:
|
||||
get:
|
||||
description: |-
|
||||
Job to get is specified by database ID
|
||||
@ -882,7 +881,7 @@ paths:
|
||||
summary: Get job meta and configurable metric data
|
||||
tags:
|
||||
- Job query
|
||||
/jobs/delete_job/:
|
||||
/api/jobs/delete_job/:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
@ -932,7 +931,7 @@ paths:
|
||||
summary: Remove a job from the sql database
|
||||
tags:
|
||||
- Job remove
|
||||
/jobs/delete_job/{id}:
|
||||
/api/jobs/delete_job/{id}:
|
||||
delete:
|
||||
description: Job to remove is specified by database ID. This will not remove
|
||||
the job from the job archive.
|
||||
@ -979,7 +978,7 @@ paths:
|
||||
summary: Remove a job from the sql database
|
||||
tags:
|
||||
- Job remove
|
||||
/jobs/delete_job_before/{ts}:
|
||||
/api/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.
|
||||
@ -1026,7 +1025,7 @@ paths:
|
||||
summary: Remove a job from the sql database
|
||||
tags:
|
||||
- Job remove
|
||||
/jobs/edit_meta/{id}:
|
||||
/api/jobs/edit_meta/{id}:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
@ -1073,7 +1072,7 @@ paths:
|
||||
summary: Edit meta-data json
|
||||
tags:
|
||||
- Job add and modify
|
||||
/jobs/start_job/:
|
||||
/api/jobs/start_job/:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
@ -1120,7 +1119,7 @@ paths:
|
||||
summary: Adds a new job as "running"
|
||||
tags:
|
||||
- Job add and modify
|
||||
/jobs/stop_job/:
|
||||
/api/jobs/stop_job/:
|
||||
post:
|
||||
description: |-
|
||||
Job to stop is specified by request body. All fields are required in this case.
|
||||
@ -1168,7 +1167,7 @@ paths:
|
||||
summary: Marks job as completed and triggers archiving
|
||||
tags:
|
||||
- Job add and modify
|
||||
/jobs/tag_job/{id}:
|
||||
/api/jobs/tag_job/{id}:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
@ -1218,173 +1217,11 @@ paths:
|
||||
summary: Adds one or more tags to a job
|
||||
tags:
|
||||
- Job add and modify
|
||||
/notice/:
|
||||
post:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
description: |-
|
||||
Modifies the content of notice.txt, shown as notice box on the homepage.
|
||||
If more than one formValue is set then only the highest priority field is used.
|
||||
Only accessible from IPs registered with apiAllowedIPs configuration option.
|
||||
parameters:
|
||||
- description: 'Priority 1: New content to display'
|
||||
in: formData
|
||||
name: new-content
|
||||
type: string
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
"200":
|
||||
description: Success Response Message
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: Forbidden
|
||||
schema:
|
||||
type: string
|
||||
"422":
|
||||
description: 'Unprocessable Entity: The user could not be updated'
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Updates or empties the notice box content
|
||||
tags:
|
||||
- User
|
||||
/user/{id}:
|
||||
post:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
description: |-
|
||||
Modifies user defined by username (id) in one of four possible ways.
|
||||
If more than one formValue is set then only the highest priority field is used.
|
||||
Only accessible from IPs registered with apiAllowedIPs configuration option.
|
||||
parameters:
|
||||
- description: Database ID of User
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: 'Priority 1: Role to add'
|
||||
enum:
|
||||
- admin
|
||||
- support
|
||||
- manager
|
||||
- user
|
||||
- api
|
||||
in: formData
|
||||
name: add-role
|
||||
type: string
|
||||
- description: 'Priority 2: Role to remove'
|
||||
enum:
|
||||
- admin
|
||||
- support
|
||||
- manager
|
||||
- user
|
||||
- api
|
||||
in: formData
|
||||
name: remove-role
|
||||
type: string
|
||||
- description: 'Priority 3: Project to add'
|
||||
in: formData
|
||||
name: add-project
|
||||
type: string
|
||||
- description: 'Priority 4: Project to remove'
|
||||
in: formData
|
||||
name: remove-project
|
||||
type: string
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
"200":
|
||||
description: Success Response Message
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: Forbidden
|
||||
schema:
|
||||
type: string
|
||||
"422":
|
||||
description: 'Unprocessable Entity: The user could not be updated'
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Updates an existing user
|
||||
tags:
|
||||
- User
|
||||
/users/:
|
||||
delete:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
description: |-
|
||||
User defined by username in form data will be deleted from database.
|
||||
Only accessible from IPs registered with apiAllowedIPs configuration option.
|
||||
parameters:
|
||||
- description: User ID to delete
|
||||
in: formData
|
||||
name: username
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
"200":
|
||||
description: User deleted successfully
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: Forbidden
|
||||
schema:
|
||||
type: string
|
||||
"422":
|
||||
description: 'Unprocessable Entity: deleting user failed'
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Deletes a user
|
||||
tags:
|
||||
- User
|
||||
/api/users/:
|
||||
get:
|
||||
description: |-
|
||||
Returns a JSON-encoded list of users.
|
||||
Required query-parameter defines if all users or only users with additional special roles are returned.
|
||||
Only accessible from IPs registered with apiAllowedIPs configuration option.
|
||||
parameters:
|
||||
- description: If returned list should contain all users or only users with
|
||||
additional special roles
|
||||
@ -1422,46 +1259,73 @@ paths:
|
||||
summary: Returns a list of users
|
||||
tags:
|
||||
- User
|
||||
post:
|
||||
/jobs/tag_job/{id}:
|
||||
delete:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
- application/json
|
||||
description: |-
|
||||
User specified in form data will be saved to database.
|
||||
Only accessible from IPs registered with apiAllowedIPs configuration option.
|
||||
Removes tag(s) from a job specified by DB ID. Name and Type of Tag(s) must match.
|
||||
Tag Scope is required for matching, options: "global", "admin". Private tags can not be deleted via API.
|
||||
If tagged job is already finished: Tag will be removed from respective archive files.
|
||||
parameters:
|
||||
- description: Unique user ID
|
||||
in: formData
|
||||
name: username
|
||||
- description: Job Database ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: User password
|
||||
in: formData
|
||||
name: password
|
||||
type: integer
|
||||
- description: Array of tag-objects to remove
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
type: string
|
||||
- description: User role
|
||||
enum:
|
||||
- admin
|
||||
- support
|
||||
- manager
|
||||
- user
|
||||
- api
|
||||
in: formData
|
||||
name: role
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/api.ApiTag'
|
||||
type: array
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Updated job resource
|
||||
schema:
|
||||
$ref: '#/definitions/schema.Job'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/api.ErrorResponse'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/api.ErrorResponse'
|
||||
"404":
|
||||
description: Job or tag does not exist
|
||||
schema:
|
||||
$ref: '#/definitions/api.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/api.ErrorResponse'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Removes one or more tags from a job
|
||||
tags:
|
||||
- Job add and modify
|
||||
/tags/:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: |-
|
||||
Removes tags by type and name. Name and Type of Tag(s) must match.
|
||||
Tag Scope is required for matching, options: "global", "admin". Private tags can not be deleted via API.
|
||||
Tag wills be removed from respective archive files.
|
||||
parameters:
|
||||
- description: Array of tag-objects to remove
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
type: string
|
||||
- description: Managed project, required for new manager role user
|
||||
in: formData
|
||||
name: project
|
||||
type: string
|
||||
- description: Users name
|
||||
in: formData
|
||||
name: name
|
||||
type: string
|
||||
- description: Users email
|
||||
in: formData
|
||||
name: email
|
||||
type: string
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/api.ApiTag'
|
||||
type: array
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
@ -1472,28 +1336,24 @@ paths:
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
type: string
|
||||
$ref: '#/definitions/api.ErrorResponse'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: Forbidden
|
||||
$ref: '#/definitions/api.ErrorResponse'
|
||||
"404":
|
||||
description: Job or tag does not exist
|
||||
schema:
|
||||
type: string
|
||||
"422":
|
||||
description: 'Unprocessable Entity: creating user failed'
|
||||
schema:
|
||||
type: string
|
||||
$ref: '#/definitions/api.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
type: string
|
||||
$ref: '#/definitions/api.ErrorResponse'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Adds a new user
|
||||
summary: Removes all tags and job-relations for type:name tuple
|
||||
tags:
|
||||
- User
|
||||
- Tag remove
|
||||
securityDefinitions:
|
||||
ApiKeyAuth:
|
||||
in: header
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||
"github.com/99designs/gqlgen/graphql/playground"
|
||||
"github.com/ClusterCockpit/cc-backend/internal/api"
|
||||
"github.com/ClusterCockpit/cc-backend/internal/archiver"
|
||||
@ -31,6 +32,7 @@ import (
|
||||
"github.com/ClusterCockpit/cc-backend/web"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
httpSwagger "github.com/swaggo/http-swagger"
|
||||
)
|
||||
|
||||
@ -53,13 +55,24 @@ func serverInit() {
|
||||
// Setup the http.Handler/Router used by the server
|
||||
graph.Init()
|
||||
resolver := graph.GetResolverInstance()
|
||||
graphQLEndpoint := handler.NewDefaultServer(
|
||||
graphQLServer := handler.New(
|
||||
generated.NewExecutableSchema(generated.Config{Resolvers: resolver}))
|
||||
|
||||
graphQLServer.AddTransport(transport.SSE{})
|
||||
graphQLServer.AddTransport(transport.POST{})
|
||||
graphQLServer.AddTransport(transport.Websocket{
|
||||
KeepAlivePingInterval: 10 * time.Second,
|
||||
Upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if os.Getenv("DEBUG") != "1" {
|
||||
// Having this handler means that a error message is returned via GraphQL instead of the connection simply beeing closed.
|
||||
// The problem with this is that then, no more stacktrace is printed to stderr.
|
||||
graphQLEndpoint.SetRecoverFunc(func(ctx context.Context, err interface{}) error {
|
||||
graphQLServer.SetRecoverFunc(func(ctx context.Context, err any) error {
|
||||
switch e := err.(type) {
|
||||
case string:
|
||||
return fmt.Errorf("MAIN > Panic: %s", e)
|
||||
@ -78,7 +91,7 @@ func serverInit() {
|
||||
router = mux.NewRouter()
|
||||
buildInfo := web.Build{Version: version, Hash: commit, Buildtime: date}
|
||||
|
||||
info := map[string]interface{}{}
|
||||
info := map[string]any{}
|
||||
info["hasOpenIDConnect"] = false
|
||||
|
||||
if config.Keys.OpenIDConfig != nil {
|
||||
@ -208,7 +221,7 @@ func serverInit() {
|
||||
router.PathPrefix("/swagger/").Handler(httpSwagger.Handler(
|
||||
httpSwagger.URL("http://" + config.Keys.Addr + "/swagger/doc.json"))).Methods(http.MethodGet)
|
||||
}
|
||||
secured.Handle("/query", graphQLEndpoint)
|
||||
secured.Handle("/query", graphQLServer)
|
||||
|
||||
// Send a searchId and then reply with a redirect to a user, or directly send query to job table for jobid and project.
|
||||
secured.HandleFunc("/search", func(rw http.ResponseWriter, r *http.Request) {
|
||||
|
1
go.mod
1
go.mod
@ -1,6 +1,7 @@
|
||||
module github.com/ClusterCockpit/cc-backend
|
||||
|
||||
go 1.23.5
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
|
@ -23,7 +23,7 @@ const docTemplate = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/clusters/": {
|
||||
"/api/clusters/": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
@ -80,7 +80,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/": {
|
||||
"/api/jobs/": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
@ -175,7 +175,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/delete_job/": {
|
||||
"/api/jobs/delete_job/": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
@ -250,7 +250,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/delete_job/{id}": {
|
||||
"/api/jobs/delete_job/{id}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
@ -320,7 +320,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/delete_job_before/{ts}": {
|
||||
"/api/jobs/delete_job_before/{ts}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
@ -390,7 +390,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/edit_meta/{id}": {
|
||||
"/api/jobs/edit_meta/{id}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
@ -460,7 +460,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/start_job/": {
|
||||
"/api/jobs/start_job/": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
@ -529,7 +529,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/stop_job/": {
|
||||
"/api/jobs/stop_job/": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
@ -601,7 +601,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/tag_job/{id}": {
|
||||
"/api/jobs/tag_job/{id}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
@ -674,7 +674,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/jobs/{id}": {
|
||||
"/api/jobs/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
@ -833,185 +833,14 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/notice/": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Modifies the content of notice.txt, shown as notice box on the homepage.\nIf more than one formValue is set then only the highest priority field is used.\nOnly accessible from IPs registered with apiAllowedIPs configuration option.",
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Updates or empties the notice box content",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Priority 1: New content to display",
|
||||
"name": "new-content",
|
||||
"in": "formData"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success Response Message",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity: The user could not be updated",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/{id}": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Modifies user defined by username (id) in one of four possible ways.\nIf more than one formValue is set then only the highest priority field is used.\nOnly accessible from IPs registered with apiAllowedIPs configuration option.",
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Updates an existing user",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Database ID of User",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"admin",
|
||||
"support",
|
||||
"manager",
|
||||
"user",
|
||||
"api"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Priority 1: Role to add",
|
||||
"name": "add-role",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"admin",
|
||||
"support",
|
||||
"manager",
|
||||
"user",
|
||||
"api"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Priority 2: Role to remove",
|
||||
"name": "remove-role",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Priority 3: Project to add",
|
||||
"name": "add-project",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Priority 4: Project to remove",
|
||||
"name": "remove-project",
|
||||
"in": "formData"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success Response Message",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity: The user could not be updated",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/": {
|
||||
"/api/users/": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a JSON-encoded list of users.\nRequired query-parameter defines if all users or only users with additional special roles are returned.\nOnly accessible from IPs registered with apiAllowedIPs configuration option.",
|
||||
"description": "Returns a JSON-encoded list of users.\nRequired query-parameter defines if all users or only users with additional special roles are returned.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@ -1063,70 +892,111 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
}
|
||||
},
|
||||
"/jobs/tag_job/{id}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "User specified in form data will be saved to database.\nOnly accessible from IPs registered with apiAllowedIPs configuration option.",
|
||||
"description": "Removes tag(s) from a job specified by DB ID. Name and Type of Tag(s) must match.\nTag Scope is required for matching, options: \"global\", \"admin\". Private tags can not be deleted via API.\nIf tagged job is already finished: Tag will be removed from respective archive files.",
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Job add and modify"
|
||||
],
|
||||
"summary": "Removes one or more tags from a job",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Job Database ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Array of tag-objects to remove",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.ApiTag"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Updated job resource",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.Job"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Job or tag does not exist",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tags/": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Removes tags by type and name. Name and Type of Tag(s) must match.\nTag Scope is required for matching, options: \"global\", \"admin\". Private tags can not be deleted via API.\nTag wills be removed from respective archive files.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
"Tag remove"
|
||||
],
|
||||
"summary": "Adds a new user",
|
||||
"summary": "Removes all tags and job-relations for type:name tuple",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Unique user ID",
|
||||
"name": "username",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User password",
|
||||
"name": "password",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"admin",
|
||||
"support",
|
||||
"manager",
|
||||
"user",
|
||||
"api"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "User role",
|
||||
"name": "role",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Managed project, required for new manager role user",
|
||||
"name": "project",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Users name",
|
||||
"name": "name",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Users email",
|
||||
"name": "email",
|
||||
"in": "formData"
|
||||
"description": "Array of tag-objects to remove",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api.ApiTag"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@ -1139,93 +1009,25 @@ const docTemplate = `{
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"404": {
|
||||
"description": "Job or tag does not exist",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity: creating user failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "User defined by username in form data will be deleted from database.\nOnly accessible from IPs registered with apiAllowedIPs configuration option.",
|
||||
"consumes": [
|
||||
"multipart/form-data"
|
||||
],
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
],
|
||||
"summary": "Deletes a user",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID to delete",
|
||||
"name": "username",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "User deleted successfully"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity: deleting user failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/api.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2191,7 +1993,7 @@ const docTemplate = `{
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "1.0.0",
|
||||
Host: "localhost:8080",
|
||||
BasePath: "/api",
|
||||
BasePath: "",
|
||||
Schemes: []string{},
|
||||
Title: "ClusterCockpit REST API",
|
||||
Description: "API for batch job control.",
|
||||
|
@ -46,7 +46,6 @@ import (
|
||||
// @license.url https://opensource.org/licenses/MIT
|
||||
|
||||
// @host localhost:8080
|
||||
// @basePath /api
|
||||
|
||||
// @securityDefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
@ -82,12 +81,15 @@ func (api *RestApi) MountApiRoutes(r *mux.Router) {
|
||||
r.HandleFunc("/jobs/{id}", api.getJobById).Methods(http.MethodPost)
|
||||
r.HandleFunc("/jobs/{id}", api.getCompleteJobById).Methods(http.MethodGet)
|
||||
r.HandleFunc("/jobs/tag_job/{id}", api.tagJob).Methods(http.MethodPost, http.MethodPatch)
|
||||
r.HandleFunc("/jobs/tag_job/{id}", api.removeTagJob).Methods(http.MethodDelete)
|
||||
r.HandleFunc("/jobs/edit_meta/{id}", api.editMeta).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)
|
||||
|
||||
r.HandleFunc("/tags/", api.removeTags).Methods(http.MethodDelete)
|
||||
|
||||
if api.MachineStateDir != "" {
|
||||
r.HandleFunc("/machine_state/{cluster}/{host}", api.getMachineState).Methods(http.MethodGet)
|
||||
r.HandleFunc("/machine_state/{cluster}/{host}", api.putMachineState).Methods(http.MethodPut, http.MethodPost)
|
||||
@ -217,7 +219,7 @@ func handleError(err error, statusCode int, rw http.ResponseWriter) {
|
||||
})
|
||||
}
|
||||
|
||||
func decode(r io.Reader, val interface{}) error {
|
||||
func decode(r io.Reader, val any) error {
|
||||
dec := json.NewDecoder(r)
|
||||
dec.DisallowUnknownFields()
|
||||
return dec.Decode(val)
|
||||
@ -235,7 +237,7 @@ func decode(r io.Reader, val interface{}) error {
|
||||
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
||||
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
|
||||
// @security ApiKeyAuth
|
||||
// @router /clusters/ [get]
|
||||
// @router /api/clusters/ [get]
|
||||
func (api *RestApi) getClusters(rw http.ResponseWriter, r *http.Request) {
|
||||
if user := repository.GetUserFromContext(r.Context()); user != nil &&
|
||||
!user.HasRole(schema.RoleApi) {
|
||||
@ -290,7 +292,7 @@ func (api *RestApi) getClusters(rw http.ResponseWriter, r *http.Request) {
|
||||
// @failure 403 {object} api.ErrorResponse "Forbidden"
|
||||
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/ [get]
|
||||
// @router /api/jobs/ [get]
|
||||
func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
|
||||
withMetadata := false
|
||||
filter := &model.JobFilter{}
|
||||
@ -424,7 +426,7 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
|
||||
// @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/{id} [get]
|
||||
// @router /api/jobs/{id} [get]
|
||||
func (api *RestApi) getCompleteJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
// Fetch job from db
|
||||
id, ok := mux.Vars(r)["id"]
|
||||
@ -517,7 +519,7 @@ func (api *RestApi) getCompleteJobById(rw http.ResponseWriter, r *http.Request)
|
||||
// @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/{id} [post]
|
||||
// @router /api/jobs/{id} [post]
|
||||
func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
// Fetch job from db
|
||||
id, ok := mux.Vars(r)["id"]
|
||||
@ -621,7 +623,7 @@ func (api *RestApi) getJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
// @failure 404 {object} api.ErrorResponse "Job does not exist"
|
||||
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/edit_meta/{id} [post]
|
||||
// @router /api/jobs/edit_meta/{id} [post]
|
||||
func (api *RestApi) editMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||
if err != nil {
|
||||
@ -667,7 +669,7 @@ func (api *RestApi) editMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
// @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]
|
||||
// @router /api/jobs/tag_job/{id} [post]
|
||||
func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||
if err != nil {
|
||||
@ -713,6 +715,114 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(rw).Encode(job)
|
||||
}
|
||||
|
||||
// removeTagJob godoc
|
||||
// @summary Removes one or more tags from a job
|
||||
// @tags Job add and modify
|
||||
// @description Removes tag(s) from a job specified by DB ID. Name and Type of Tag(s) must match.
|
||||
// @description Tag Scope is required for matching, options: "global", "admin". Private tags can not be deleted via API.
|
||||
// @description If tagged job is already finished: Tag will be removed from 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 remove"
|
||||
// @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} [delete]
|
||||
func (api *RestApi) removeTagJob(rw http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
job, err := api.JobRepository.FindById(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
job.Tags, err = api.JobRepository.GetTags(repository.GetUserFromContext(r.Context()), &job.ID)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req TagJobApiRequest
|
||||
if err := decode(r.Body, &req); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
for _, rtag := range req {
|
||||
// Only Global and Admin Tags
|
||||
if rtag.Scope != "global" && rtag.Scope != "admin" {
|
||||
log.Warnf("Cannot delete private tag for job %d: Skip", job.JobID)
|
||||
continue
|
||||
}
|
||||
|
||||
remainingTags, err := api.JobRepository.RemoveJobTagByRequest(repository.GetUserFromContext(r.Context()), job.ID, rtag.Type, rtag.Name, rtag.Scope)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
job.Tags = remainingTags
|
||||
}
|
||||
|
||||
rw.Header().Add("Content-Type", "application/json")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(rw).Encode(job)
|
||||
}
|
||||
|
||||
// removeTags godoc
|
||||
// @summary Removes all tags and job-relations for type:name tuple
|
||||
// @tags Tag remove
|
||||
// @description Removes tags by type and name. Name and Type of Tag(s) must match.
|
||||
// @description Tag Scope is required for matching, options: "global", "admin". Private tags can not be deleted via API.
|
||||
// @description Tag wills be removed from respective archive files.
|
||||
// @accept json
|
||||
// @produce plain
|
||||
// @param request body api.TagJobApiRequest true "Array of tag-objects to remove"
|
||||
// @success 200 {string} string "Success Response"
|
||||
// @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 /tags/ [delete]
|
||||
func (api *RestApi) removeTags(rw http.ResponseWriter, r *http.Request) {
|
||||
var req TagJobApiRequest
|
||||
if err := decode(r.Body, &req); err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
targetCount := len(req)
|
||||
currentCount := 0
|
||||
for _, rtag := range req {
|
||||
// Only Global and Admin Tags
|
||||
if rtag.Scope != "global" && rtag.Scope != "admin" {
|
||||
log.Warn("Cannot delete private tag: Skip")
|
||||
continue
|
||||
}
|
||||
|
||||
err := api.JobRepository.RemoveTagByRequest(rtag.Type, rtag.Name, rtag.Scope)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
} else {
|
||||
currentCount++
|
||||
}
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.Write([]byte(fmt.Sprintf("Deleted Tags from DB: %d successfull of %d requested\n", currentCount, targetCount)))
|
||||
}
|
||||
|
||||
// startJob godoc
|
||||
// @summary Adds a new job as "running"
|
||||
// @tags Job add and modify
|
||||
@ -728,7 +838,7 @@ func (api *RestApi) tagJob(rw http.ResponseWriter, r *http.Request) {
|
||||
// @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]
|
||||
// @router /api/jobs/start_job/ [post]
|
||||
func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
|
||||
req := schema.JobMeta{BaseJob: schema.JobDefaults}
|
||||
if err := decode(r.Body, &req); err != nil {
|
||||
@ -801,7 +911,7 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
|
||||
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: job has already been stopped"
|
||||
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
|
||||
// @security ApiKeyAuth
|
||||
// @router /jobs/stop_job/ [post]
|
||||
// @router /api/jobs/stop_job/ [post]
|
||||
func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
||||
// Parse request body
|
||||
req := StopJobApiRequest{}
|
||||
@ -842,7 +952,7 @@ func (api *RestApi) stopJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
||||
// @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]
|
||||
// @router /api/jobs/delete_job/{id} [delete]
|
||||
func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
// Fetch job (that will be stopped) from db
|
||||
id, ok := mux.Vars(r)["id"]
|
||||
@ -885,7 +995,7 @@ func (api *RestApi) deleteJobById(rw http.ResponseWriter, r *http.Request) {
|
||||
// @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]
|
||||
// @router /api/jobs/delete_job/ [delete]
|
||||
func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request) {
|
||||
// Parse request body
|
||||
req := DeleteJobApiRequest{}
|
||||
@ -935,7 +1045,7 @@ func (api *RestApi) deleteJobByRequest(rw http.ResponseWriter, r *http.Request)
|
||||
// @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]
|
||||
// @router /api/jobs/delete_job_before/{ts} [delete]
|
||||
func (api *RestApi) deleteJobBefore(rw http.ResponseWriter, r *http.Request) {
|
||||
var cnt int
|
||||
// Fetch job (that will be stopped) from db
|
||||
@ -1053,26 +1163,6 @@ func (api *RestApi) getJobMetrics(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// createUser godoc
|
||||
// @summary Adds a new user
|
||||
// @tags User
|
||||
// @description User specified in form data will be saved to database.
|
||||
// @accept mpfd
|
||||
// @produce plain
|
||||
// @param username formData string true "Unique user ID"
|
||||
// @param password formData string true "User password"
|
||||
// @param role formData string true "User role" Enums(admin, support, manager, user, api)
|
||||
// @param project formData string false "Managed project, required for new manager role user"
|
||||
// @param name formData string false "Users name"
|
||||
// @param email formData string false "Users email"
|
||||
// @success 200 {string} string "Success Response"
|
||||
// @failure 400 {string} string "Bad Request"
|
||||
// @failure 401 {string} string "Unauthorized"
|
||||
// @failure 403 {string} string "Forbidden"
|
||||
// @failure 422 {string} string "Unprocessable Entity: creating user failed"
|
||||
// @failure 500 {string} string "Internal Server Error"
|
||||
// @security ApiKeyAuth
|
||||
// @router /users/ [post]
|
||||
func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
|
||||
// SecuredCheck() only worked with TokenAuth: Removed
|
||||
|
||||
@ -1117,21 +1207,6 @@ func (api *RestApi) createUser(rw http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(rw, "User %v successfully created!\n", username)
|
||||
}
|
||||
|
||||
// deleteUser godoc
|
||||
// @summary Deletes a user
|
||||
// @tags User
|
||||
// @description User defined by username in form data will be deleted from database.
|
||||
// @accept mpfd
|
||||
// @produce plain
|
||||
// @param username formData string true "User ID to delete"
|
||||
// @success 200 "User deleted successfully"
|
||||
// @failure 400 {string} string "Bad Request"
|
||||
// @failure 401 {string} string "Unauthorized"
|
||||
// @failure 403 {string} string "Forbidden"
|
||||
// @failure 422 {string} string "Unprocessable Entity: deleting user failed"
|
||||
// @failure 500 {string} string "Internal Server Error"
|
||||
// @security ApiKeyAuth
|
||||
// @router /users/ [delete]
|
||||
func (api *RestApi) deleteUser(rw http.ResponseWriter, r *http.Request) {
|
||||
// SecuredCheck() only worked with TokenAuth: Removed
|
||||
|
||||
@ -1162,7 +1237,7 @@ func (api *RestApi) deleteUser(rw http.ResponseWriter, r *http.Request) {
|
||||
// @failure 403 {string} string "Forbidden"
|
||||
// @failure 500 {string} string "Internal Server Error"
|
||||
// @security ApiKeyAuth
|
||||
// @router /users/ [get]
|
||||
// @router /api/users/ [get]
|
||||
func (api *RestApi) getUsers(rw http.ResponseWriter, r *http.Request) {
|
||||
// SecuredCheck() only worked with TokenAuth: Removed
|
||||
|
||||
@ -1180,26 +1255,6 @@ func (api *RestApi) getUsers(rw http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(rw).Encode(users)
|
||||
}
|
||||
|
||||
// updateUser godoc
|
||||
// @summary Updates an existing user
|
||||
// @tags User
|
||||
// @description Modifies user defined by username (id) in one of four possible ways.
|
||||
// @description If more than one formValue is set then only the highest priority field is used.
|
||||
// @accept mpfd
|
||||
// @produce plain
|
||||
// @param id path string true "Database ID of User"
|
||||
// @param add-role formData string false "Priority 1: Role to add" Enums(admin, support, manager, user, api)
|
||||
// @param remove-role formData string false "Priority 2: Role to remove" Enums(admin, support, manager, user, api)
|
||||
// @param add-project formData string false "Priority 3: Project to add"
|
||||
// @param remove-project formData string false "Priority 4: Project to remove"
|
||||
// @success 200 {string} string "Success Response Message"
|
||||
// @failure 400 {string} string "Bad Request"
|
||||
// @failure 401 {string} string "Unauthorized"
|
||||
// @failure 403 {string} string "Forbidden"
|
||||
// @failure 422 {string} string "Unprocessable Entity: The user could not be updated"
|
||||
// @failure 500 {string} string "Internal Server Error"
|
||||
// @security ApiKeyAuth
|
||||
// @router /user/{id} [post]
|
||||
func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
|
||||
// SecuredCheck() only worked with TokenAuth: Removed
|
||||
|
||||
@ -1244,22 +1299,6 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// editNotice godoc
|
||||
// @summary Updates or empties the notice box content
|
||||
// @tags User
|
||||
// @description Modifies the content of notice.txt, shown as notice box on the homepage.
|
||||
// @description If more than one formValue is set then only the highest priority field is used.
|
||||
// @accept mpfd
|
||||
// @produce plain
|
||||
// @param new-content formData string false "Priority 1: New content to display"
|
||||
// @success 200 {string} string "Success Response Message"
|
||||
// @failure 400 {string} string "Bad Request"
|
||||
// @failure 401 {string} string "Unauthorized"
|
||||
// @failure 403 {string} string "Forbidden"
|
||||
// @failure 422 {string} string "Unprocessable Entity: The user could not be updated"
|
||||
// @failure 500 {string} string "Internal Server Error"
|
||||
// @security ApiKeyAuth
|
||||
// @router /notice/ [post]
|
||||
func (api *RestApi) editNotice(rw http.ResponseWriter, r *http.Request) {
|
||||
// SecuredCheck() only worked with TokenAuth: Removed
|
||||
|
||||
|
@ -250,6 +250,7 @@ type ComplexityRoot struct {
|
||||
AddTagsToJob func(childComplexity int, job string, tagIds []string) int
|
||||
CreateTag func(childComplexity int, typeArg string, name string, scope string) int
|
||||
DeleteTag func(childComplexity int, id string) int
|
||||
RemoveTagFromList func(childComplexity int, tagIds []string) int
|
||||
RemoveTagsFromJob func(childComplexity int, job string, tagIds []string) int
|
||||
UpdateConfiguration func(childComplexity int, name string, value string) int
|
||||
}
|
||||
@ -399,6 +400,7 @@ type MutationResolver interface {
|
||||
DeleteTag(ctx context.Context, id string) (string, error)
|
||||
AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error)
|
||||
RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error)
|
||||
RemoveTagFromList(ctx context.Context, tagIds []string) ([]int, error)
|
||||
UpdateConfiguration(ctx context.Context, name string, value string) (*string, error)
|
||||
}
|
||||
type QueryResolver interface {
|
||||
@ -1310,6 +1312,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Mutation.DeleteTag(childComplexity, args["id"].(string)), true
|
||||
|
||||
case "Mutation.removeTagFromList":
|
||||
if e.complexity.Mutation.RemoveTagFromList == nil {
|
||||
break
|
||||
}
|
||||
|
||||
args, err := ec.field_Mutation_removeTagFromList_args(context.TODO(), rawArgs)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Mutation.RemoveTagFromList(childComplexity, args["tagIds"].([]string)), true
|
||||
|
||||
case "Mutation.removeTagsFromJob":
|
||||
if e.complexity.Mutation.RemoveTagsFromJob == nil {
|
||||
break
|
||||
@ -2339,6 +2353,7 @@ type Mutation {
|
||||
deleteTag(id: ID!): ID!
|
||||
addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]!
|
||||
removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]!
|
||||
removeTagFromList(tagIds: [ID!]!): [Int!]!
|
||||
|
||||
updateConfiguration(name: String!, value: String!): String
|
||||
}
|
||||
@ -2617,6 +2632,34 @@ func (ec *executionContext) field_Mutation_deleteTag_argsID(
|
||||
return zeroVal, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_removeTagFromList_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||
var err error
|
||||
args := map[string]any{}
|
||||
arg0, err := ec.field_Mutation_removeTagFromList_argsTagIds(ctx, rawArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args["tagIds"] = arg0
|
||||
return args, nil
|
||||
}
|
||||
func (ec *executionContext) field_Mutation_removeTagFromList_argsTagIds(
|
||||
ctx context.Context,
|
||||
rawArgs map[string]any,
|
||||
) ([]string, error) {
|
||||
if _, ok := rawArgs["tagIds"]; !ok {
|
||||
var zeroVal []string
|
||||
return zeroVal, nil
|
||||
}
|
||||
|
||||
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("tagIds"))
|
||||
if tmp, ok := rawArgs["tagIds"]; ok {
|
||||
return ec.unmarshalNID2ᚕstringᚄ(ctx, tmp)
|
||||
}
|
||||
|
||||
var zeroVal []string
|
||||
return zeroVal, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_removeTagsFromJob_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||
var err error
|
||||
args := map[string]any{}
|
||||
@ -9690,6 +9733,61 @@ func (ec *executionContext) fieldContext_Mutation_removeTagsFromJob(ctx context.
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_removeTagFromList(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_Mutation_removeTagFromList(ctx, field)
|
||||
if err != nil {
|
||||
return graphql.Null
|
||||
}
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Mutation().RemoveTagFromList(rctx, fc.Args["tagIds"].([]string))
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.([]int)
|
||||
fc.Result = res
|
||||
return ec.marshalNInt2ᚕintᚄ(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_Mutation_removeTagFromList(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "Mutation",
|
||||
Field: field,
|
||||
IsMethod: true,
|
||||
IsResolver: true,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type Int does not have child fields")
|
||||
},
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = ec.Recover(ctx, r)
|
||||
ec.Error(ctx, err)
|
||||
}
|
||||
}()
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
if fc.Args, err = ec.field_Mutation_removeTagFromList_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return fc, err
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_updateConfiguration(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_Mutation_updateConfiguration(ctx, field)
|
||||
if err != nil {
|
||||
@ -17765,6 +17863,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "removeTagFromList":
|
||||
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||
return ec._Mutation_removeTagFromList(ctx, field)
|
||||
})
|
||||
if out.Values[i] == graphql.Null {
|
||||
out.Invalids++
|
||||
}
|
||||
case "updateConfiguration":
|
||||
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
|
||||
return ec._Mutation_updateConfiguration(ctx, field)
|
||||
|
@ -125,23 +125,41 @@ func (r *metricValueResolver) Name(ctx context.Context, obj *schema.MetricValue)
|
||||
|
||||
// CreateTag is the resolver for the createTag field.
|
||||
func (r *mutationResolver) CreateTag(ctx context.Context, typeArg string, name string, scope string) (*schema.Tag, error) {
|
||||
id, err := r.Repo.CreateTag(typeArg, name, scope)
|
||||
if err != nil {
|
||||
log.Warn("Error while creating tag")
|
||||
return nil, err
|
||||
user := repository.GetUserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("no user in context")
|
||||
}
|
||||
|
||||
return &schema.Tag{ID: id, Type: typeArg, Name: name, Scope: scope}, nil
|
||||
// Test Access: Admins && Admin Tag OR Support/Admin and Global Tag OR Everyone && Private Tag
|
||||
if user.HasRole(schema.RoleAdmin) && scope == "admin" ||
|
||||
user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) && scope == "global" ||
|
||||
user.Username == scope {
|
||||
// Create in DB
|
||||
id, err := r.Repo.CreateTag(typeArg, name, scope)
|
||||
if err != nil {
|
||||
log.Warn("Error while creating tag")
|
||||
return nil, err
|
||||
}
|
||||
return &schema.Tag{ID: id, Type: typeArg, Name: name, Scope: scope}, nil
|
||||
} else {
|
||||
log.Warnf("Not authorized to create tag with scope: %s", scope)
|
||||
return nil, fmt.Errorf("Not authorized to create tag with scope: %s", scope)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteTag is the resolver for the deleteTag field.
|
||||
func (r *mutationResolver) DeleteTag(ctx context.Context, id string) (string, error) {
|
||||
// This Uses ID string <-> ID string, removeTagFromList uses []string <-> []int
|
||||
panic(fmt.Errorf("not implemented: DeleteTag - deleteTag"))
|
||||
}
|
||||
|
||||
// AddTagsToJob is the resolver for the addTagsToJob field.
|
||||
func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) {
|
||||
// Selectable Tags Pre-Filtered by Scope in Frontend: No backend check required
|
||||
user := repository.GetUserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("no user in context")
|
||||
}
|
||||
|
||||
jid, err := strconv.ParseInt(job, 10, 64)
|
||||
if err != nil {
|
||||
log.Warn("Error while adding tag to job")
|
||||
@ -150,15 +168,32 @@ func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds
|
||||
|
||||
tags := []*schema.Tag{}
|
||||
for _, tagId := range tagIds {
|
||||
// Get ID
|
||||
tid, err := strconv.ParseInt(tagId, 10, 64)
|
||||
if err != nil {
|
||||
log.Warn("Error while parsing tag id")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tags, err = r.Repo.AddTag(repository.GetUserFromContext(ctx), jid, tid); err != nil {
|
||||
log.Warn("Error while adding tag")
|
||||
return nil, err
|
||||
// Test Exists
|
||||
_, _, tscope, exists := r.Repo.TagInfo(tid)
|
||||
if !exists {
|
||||
log.Warnf("Tag does not exist (ID): %d", tid)
|
||||
return nil, fmt.Errorf("Tag does not exist (ID): %d", tid)
|
||||
}
|
||||
|
||||
// Test Access: Admins && Admin Tag OR Support/Admin and Global Tag OR Everyone && Private Tag
|
||||
if user.HasRole(schema.RoleAdmin) && tscope == "admin" ||
|
||||
user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) && tscope == "global" ||
|
||||
user.Username == tscope {
|
||||
// Add to Job
|
||||
if tags, err = r.Repo.AddTag(user, jid, tid); err != nil {
|
||||
log.Warn("Error while adding tag")
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
log.Warnf("Not authorized to add tag: %d", tid)
|
||||
return nil, fmt.Errorf("Not authorized to add tag: %d", tid)
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,7 +202,11 @@ func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds
|
||||
|
||||
// RemoveTagsFromJob is the resolver for the removeTagsFromJob field.
|
||||
func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) {
|
||||
// Removable Tags Pre-Filtered by Scope in Frontend: No backend check required
|
||||
user := repository.GetUserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("no user in context")
|
||||
}
|
||||
|
||||
jid, err := strconv.ParseInt(job, 10, 64)
|
||||
if err != nil {
|
||||
log.Warn("Error while parsing job id")
|
||||
@ -176,21 +215,80 @@ func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, ta
|
||||
|
||||
tags := []*schema.Tag{}
|
||||
for _, tagId := range tagIds {
|
||||
// Get ID
|
||||
tid, err := strconv.ParseInt(tagId, 10, 64)
|
||||
if err != nil {
|
||||
log.Warn("Error while parsing tag id")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tags, err = r.Repo.RemoveTag(repository.GetUserFromContext(ctx), jid, tid); err != nil {
|
||||
log.Warn("Error while removing tag")
|
||||
return nil, err
|
||||
// Test Exists
|
||||
_, _, tscope, exists := r.Repo.TagInfo(tid)
|
||||
if !exists {
|
||||
log.Warnf("Tag does not exist (ID): %d", tid)
|
||||
return nil, fmt.Errorf("Tag does not exist (ID): %d", tid)
|
||||
}
|
||||
|
||||
// Test Access: Admins && Admin Tag OR Support/Admin and Global Tag OR Everyone && Private Tag
|
||||
if user.HasRole(schema.RoleAdmin) && tscope == "admin" ||
|
||||
user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) && tscope == "global" ||
|
||||
user.Username == tscope {
|
||||
// Remove from Job
|
||||
if tags, err = r.Repo.RemoveTag(user, jid, tid); err != nil {
|
||||
log.Warn("Error while removing tag")
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
log.Warnf("Not authorized to remove tag: %d", tid)
|
||||
return nil, fmt.Errorf("Not authorized to remove tag: %d", tid)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// RemoveTagFromList is the resolver for the removeTagFromList field.
|
||||
func (r *mutationResolver) RemoveTagFromList(ctx context.Context, tagIds []string) ([]int, error) {
|
||||
// Needs Contextuser
|
||||
user := repository.GetUserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("no user in context")
|
||||
}
|
||||
|
||||
tags := []int{}
|
||||
for _, tagId := range tagIds {
|
||||
// Get ID
|
||||
tid, err := strconv.ParseInt(tagId, 10, 64)
|
||||
if err != nil {
|
||||
log.Warn("Error while parsing tag id for removal")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Test Exists
|
||||
_, _, tscope, exists := r.Repo.TagInfo(tid)
|
||||
if !exists {
|
||||
log.Warnf("Tag does not exist (ID): %d", tid)
|
||||
return nil, fmt.Errorf("Tag does not exist (ID): %d", tid)
|
||||
}
|
||||
|
||||
// Test Access: Admins && Admin Tag OR Everyone && Private Tag
|
||||
if user.HasRole(schema.RoleAdmin) && (tscope == "global" || tscope == "admin") || user.Username == tscope {
|
||||
// Remove from DB
|
||||
if err = r.Repo.RemoveTagById(tid); err != nil {
|
||||
log.Warn("Error while removing tag")
|
||||
return nil, err
|
||||
} else {
|
||||
tags = append(tags, int(tid))
|
||||
}
|
||||
} else {
|
||||
log.Warnf("Not authorized to remove tag: %d", tid)
|
||||
return nil, fmt.Errorf("Not authorized to remove tag: %d", tid)
|
||||
}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// UpdateConfiguration is the resolver for the updateConfiguration field.
|
||||
func (r *mutationResolver) UpdateConfiguration(ctx context.Context, name string, value string) (*string, error) {
|
||||
if err := repository.GetUserCfgRepo().UpdateConfig(name, value, repository.GetUserFromContext(ctx)); err != nil {
|
||||
|
@ -302,6 +302,20 @@ func (ccms *CCMetricStore) buildQueries(
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if metric is removed for subcluster
|
||||
if len(mc.SubClusters) != 0 {
|
||||
isRemoved := false
|
||||
for _, scConfig := range mc.SubClusters {
|
||||
if scConfig.Name == job.SubCluster && scConfig.Remove == true {
|
||||
isRemoved = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isRemoved {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid duplicates...
|
||||
handledScopes := make([]schema.MetricScope, 0, 3)
|
||||
|
||||
@ -985,6 +999,20 @@ func (ccms *CCMetricStore) buildNodeQueries(
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if metric is removed for subcluster
|
||||
if mc.SubClusters != nil {
|
||||
isRemoved := false
|
||||
for _, scConfig := range mc.SubClusters {
|
||||
if scConfig.Name == subCluster && scConfig.Remove == true {
|
||||
isRemoved = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isRemoved {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid duplicates...
|
||||
handledScopes := make([]schema.MetricScope, 0, 3)
|
||||
|
||||
|
@ -45,7 +45,7 @@ func (r *JobRepository) AddTag(user *schema.User, job int64, tag int64) ([]*sche
|
||||
return tags, archive.UpdateTags(j, archiveTags)
|
||||
}
|
||||
|
||||
// Removes a tag from a job
|
||||
// Removes a tag from a job by tag id
|
||||
func (r *JobRepository) RemoveTag(user *schema.User, job, tag int64) ([]*schema.Tag, error) {
|
||||
j, err := r.FindByIdWithUser(user, job)
|
||||
if err != nil {
|
||||
@ -76,6 +76,99 @@ func (r *JobRepository) RemoveTag(user *schema.User, job, tag int64) ([]*schema.
|
||||
return tags, archive.UpdateTags(j, archiveTags)
|
||||
}
|
||||
|
||||
// Removes a tag from a job by tag info
|
||||
func (r *JobRepository) RemoveJobTagByRequest(user *schema.User, job int64, tagType string, tagName string, tagScope string) ([]*schema.Tag, error) {
|
||||
// Get Tag ID to delete
|
||||
tagID, exists := r.TagId(tagType, tagName, tagScope)
|
||||
if !exists {
|
||||
log.Warnf("Tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope)
|
||||
return nil, fmt.Errorf("Tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope)
|
||||
}
|
||||
|
||||
// Get Job
|
||||
j, err := r.FindByIdWithUser(user, job)
|
||||
if err != nil {
|
||||
log.Warn("Error while finding job by id")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Handle Delete
|
||||
q := sq.Delete("jobtag").Where("jobtag.job_id = ?", job).Where("jobtag.tag_id = ?", tagID)
|
||||
|
||||
if _, err := q.RunWith(r.stmtCache).Exec(); err != nil {
|
||||
s, _, _ := q.ToSql()
|
||||
log.Errorf("Error removing tag from table 'jobTag' with %s: %v", s, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := r.GetTags(user, &job)
|
||||
if err != nil {
|
||||
log.Warn("Error while getting tags for job")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
archiveTags, err := r.getArchiveTags(&job)
|
||||
if err != nil {
|
||||
log.Warn("Error while getting tags for job")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tags, archive.UpdateTags(j, archiveTags)
|
||||
}
|
||||
|
||||
// Removes a tag from db by tag info
|
||||
func (r *JobRepository) RemoveTagByRequest(tagType string, tagName string, tagScope string) error {
|
||||
// Get Tag ID to delete
|
||||
tagID, exists := r.TagId(tagType, tagName, tagScope)
|
||||
if !exists {
|
||||
log.Warnf("Tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope)
|
||||
return fmt.Errorf("Tag does not exist (name, type, scope): %s, %s, %s", tagName, tagType, tagScope)
|
||||
}
|
||||
|
||||
// Handle Delete JobTagTable
|
||||
qJobTag := sq.Delete("jobtag").Where("jobtag.tag_id = ?", tagID)
|
||||
|
||||
if _, err := qJobTag.RunWith(r.stmtCache).Exec(); err != nil {
|
||||
s, _, _ := qJobTag.ToSql()
|
||||
log.Errorf("Error removing tag from table 'jobTag' with %s: %v", s, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle Delete TagTable
|
||||
qTag := sq.Delete("tag").Where("tag.id = ?", tagID)
|
||||
|
||||
if _, err := qTag.RunWith(r.stmtCache).Exec(); err != nil {
|
||||
s, _, _ := qTag.ToSql()
|
||||
log.Errorf("Error removing tag from table 'tag' with %s: %v", s, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Removes a tag from db by tag id
|
||||
func (r *JobRepository) RemoveTagById(tagID int64) error {
|
||||
// Handle Delete JobTagTable
|
||||
qJobTag := sq.Delete("jobtag").Where("jobtag.tag_id = ?", tagID)
|
||||
|
||||
if _, err := qJobTag.RunWith(r.stmtCache).Exec(); err != nil {
|
||||
s, _, _ := qJobTag.ToSql()
|
||||
log.Errorf("Error removing tag from table 'jobTag' with %s: %v", s, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle Delete TagTable
|
||||
qTag := sq.Delete("tag").Where("tag.id = ?", tagID)
|
||||
|
||||
if _, err := qTag.RunWith(r.stmtCache).Exec(); err != nil {
|
||||
s, _, _ := qTag.ToSql()
|
||||
log.Errorf("Error removing tag from table 'tag' with %s: %v", s, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateTag creates a new tag with the specified type and name and returns its database id.
|
||||
func (r *JobRepository) CreateTag(tagType string, tagName string, tagScope string) (tagId int64, err error) {
|
||||
// Default to "Global" scope if none defined
|
||||
@ -209,6 +302,16 @@ func (r *JobRepository) TagId(tagType string, tagName string, tagScope string) (
|
||||
return
|
||||
}
|
||||
|
||||
// TagInfo returns the database infos of the tag with the specified id.
|
||||
func (r *JobRepository) TagInfo(tagId int64) (tagType string, tagName string, tagScope string, exists bool) {
|
||||
exists = true
|
||||
if err := sq.Select("tag.tag_type", "tag.tag_name", "tag.tag_scope").From("tag").Where("tag.id = ?", tagId).
|
||||
RunWith(r.stmtCache).QueryRow().Scan(&tagType, &tagName, &tagScope); err != nil {
|
||||
exists = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetTags returns a list of all scoped tags if job is nil or of the tags that the job with that database ID has.
|
||||
func (r *JobRepository) GetTags(user *schema.User, job *int64) ([]*schema.Tag, error) {
|
||||
q := sq.Select("id", "tag_type", "tag_name", "tag_scope").From("tag")
|
||||
|
@ -68,8 +68,23 @@ func initClusterConfig() error {
|
||||
}
|
||||
|
||||
for _, sc := range cluster.SubClusters {
|
||||
newMetric := mc
|
||||
newMetric.SubClusters = nil
|
||||
newMetric := &schema.MetricConfig{
|
||||
Unit: mc.Unit,
|
||||
Energy: mc.Energy,
|
||||
Name: mc.Name,
|
||||
Scope: mc.Scope,
|
||||
Aggregation: mc.Aggregation,
|
||||
Peak: mc.Peak,
|
||||
Caution: mc.Caution,
|
||||
Alert: mc.Alert,
|
||||
Timestep: mc.Timestep,
|
||||
Normal: mc.Normal,
|
||||
LowerIsBetter: mc.LowerIsBetter,
|
||||
}
|
||||
|
||||
if mc.Footprint != "" {
|
||||
newMetric.Footprint = mc.Footprint
|
||||
}
|
||||
|
||||
if cfg, ok := scLookup[sc.Name]; ok {
|
||||
if !cfg.Remove {
|
||||
|
@ -25,10 +25,18 @@
|
||||
},
|
||||
"scope": {
|
||||
"description": "Native measurement resolution",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"node",
|
||||
"socket",
|
||||
"memoryDomain",
|
||||
"core",
|
||||
"hwthread",
|
||||
"accelerator"
|
||||
]
|
||||
},
|
||||
"timestep": {
|
||||
"description": "Frequency of timeseries points",
|
||||
"description": "Frequency of timeseries points in seconds",
|
||||
"type": "integer"
|
||||
},
|
||||
"aggregation": {
|
||||
@ -108,15 +116,19 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"peak": {
|
||||
"description": "The maximum possible metric value",
|
||||
"type": "number"
|
||||
},
|
||||
"normal": {
|
||||
"description": "A common metric value level",
|
||||
"type": "number"
|
||||
},
|
||||
"caution": {
|
||||
"description": "Metric value requires attention",
|
||||
"type": "number"
|
||||
},
|
||||
"alert": {
|
||||
"description": "Metric value requiring immediate attention",
|
||||
"type": "number"
|
||||
},
|
||||
"remove": {
|
||||
|
@ -17,6 +17,7 @@
|
||||
"IPC",
|
||||
"Hz",
|
||||
"W",
|
||||
"J",
|
||||
"°C",
|
||||
""
|
||||
]
|
||||
|
@ -85,6 +85,7 @@ func IsValidRole(role string) bool {
|
||||
return getRoleEnum(role) != RoleError
|
||||
}
|
||||
|
||||
// Check if User has SPECIFIED role AND role is VALID
|
||||
func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
|
||||
if IsValidRole(role) {
|
||||
for _, r := range u.Roles {
|
||||
@ -97,6 +98,7 @@ func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
|
||||
return false, false
|
||||
}
|
||||
|
||||
// Check if User has SPECIFIED role
|
||||
func (u *User) HasRole(role Role) bool {
|
||||
for _, r := range u.Roles {
|
||||
if r == GetRoleString(role) {
|
||||
@ -106,7 +108,7 @@ func (u *User) HasRole(role Role) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Role-Arrays are short: performance not impacted by nested loop
|
||||
// Check if User has ANY of the listed roles
|
||||
func (u *User) HasAnyRole(queryroles []Role) bool {
|
||||
for _, ur := range u.Roles {
|
||||
for _, qr := range queryroles {
|
||||
@ -118,7 +120,7 @@ func (u *User) HasAnyRole(queryroles []Role) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Role-Arrays are short: performance not impacted by nested loop
|
||||
// Check if User has ALL of the listed roles
|
||||
func (u *User) HasAllRoles(queryroles []Role) bool {
|
||||
target := len(queryroles)
|
||||
matches := 0
|
||||
@ -138,7 +140,7 @@ func (u *User) HasAllRoles(queryroles []Role) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Role-Arrays are short: performance not impacted by nested loop
|
||||
// Check if User has NONE of the listed roles
|
||||
func (u *User) HasNotRoles(queryroles []Role) bool {
|
||||
matches := 0
|
||||
for _, ur := range u.Roles {
|
||||
|
@ -62,6 +62,7 @@ export default [
|
||||
entrypoint('jobs', 'src/jobs.entrypoint.js'),
|
||||
entrypoint('user', 'src/user.entrypoint.js'),
|
||||
entrypoint('list', 'src/list.entrypoint.js'),
|
||||
entrypoint('taglist', 'src/tags.entrypoint.js'),
|
||||
entrypoint('job', 'src/job.entrypoint.js'),
|
||||
entrypoint('systems', 'src/systems.entrypoint.js'),
|
||||
entrypoint('node', 'src/node.entrypoint.js'),
|
||||
|
@ -26,6 +26,8 @@
|
||||
init,
|
||||
convert2uplot,
|
||||
binsFromFootprint,
|
||||
scramble,
|
||||
scrambleNames,
|
||||
} from "./generic/utils.js";
|
||||
import PlotSelection from "./analysis/PlotSelection.svelte";
|
||||
import Filters from "./generic/Filters.svelte";
|
||||
@ -395,7 +397,7 @@
|
||||
quantities={$topQuery.data.topList.map(
|
||||
(t) => t[sortSelection.key],
|
||||
)}
|
||||
entities={$topQuery.data.topList.map((t) => t.id)}
|
||||
entities={$topQuery.data.topList.map((t) => scrambleNames ? scramble(t.id) : t.id)}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
@ -428,21 +430,21 @@
|
||||
{#if groupSelection.key == "user"}
|
||||
<th scope="col" id="topName-{te.id}"
|
||||
><a href="/monitoring/user/{te.id}?cluster={clusterName}"
|
||||
>{te.id}</a
|
||||
>{scrambleNames ? scramble(te.id) : te.id}</a
|
||||
></th
|
||||
>
|
||||
{#if te?.name}
|
||||
<Tooltip
|
||||
target={`topName-${te.id}`}
|
||||
placement="left"
|
||||
>{te.name}</Tooltip
|
||||
>{scrambleNames ? scramble(te.name) : te.name}</Tooltip
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
<th scope="col"
|
||||
><a
|
||||
href="/monitoring/jobs/?cluster={clusterName}&project={te.id}&projectMatch=eq"
|
||||
>{te.id}</a
|
||||
>{scrambleNames ? scramble(te.id) : te.id}</a
|
||||
></th
|
||||
>
|
||||
{/if}
|
||||
|
@ -128,14 +128,13 @@
|
||||
const pendingMetrics = (
|
||||
ccconfig[`job_view_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
|
||||
ccconfig[`job_view_selectedMetrics:${job.cluster}`]
|
||||
) ||
|
||||
$initq.data.globalMetrics
|
||||
.reduce((names, gm) => {
|
||||
if (gm.availability.find((av) => av.cluster === job.cluster && av.subClusters.includes(job.subCluster))) {
|
||||
names.push(gm.name);
|
||||
}
|
||||
return names;
|
||||
}, [])
|
||||
) ||
|
||||
$initq.data.globalMetrics.reduce((names, gm) => {
|
||||
if (gm.availability.find((av) => av.cluster === job.cluster && av.subClusters.includes(job.subCluster))) {
|
||||
names.push(gm.name);
|
||||
}
|
||||
return names;
|
||||
}, [])
|
||||
|
||||
// Select default Scopes to load: Check before if any metric has accelerator scope by default
|
||||
const accScopeDefault = [...pendingMetrics].some(function (m) {
|
||||
@ -338,10 +337,25 @@
|
||||
scopes={item.data.map((x) => x.scope)}
|
||||
isShared={$initq.data.job.exclusive != 1}
|
||||
/>
|
||||
{:else if item.disabled == true}
|
||||
<Card color="info">
|
||||
<CardHeader class="mb-0">
|
||||
<b>Disabled Metric</b>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<p>Metric <b>{item.metric}</b> is disabled for subcluster <b>{$initq.data.job.subCluster}</b>.</p>
|
||||
<p class="mb-1">To remove this card, open metric selection and press "Close and Apply".</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{:else}
|
||||
<Card body color="warning" class="mt-2"
|
||||
>No dataset returned for <code>{item.metric}</code></Card
|
||||
>
|
||||
<Card color="warning" class="mt-2">
|
||||
<CardHeader class="mb-0">
|
||||
<b>Missing Metric</b>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<p class="mb-1">No dataset returned for <b>{item.metric}</b>.</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
</PlotGrid>
|
||||
{/if}
|
||||
|
@ -31,6 +31,8 @@
|
||||
init,
|
||||
convert2uplot,
|
||||
transformPerNodeDataForRoofline,
|
||||
scramble,
|
||||
scrambleNames,
|
||||
} from "./generic/utils.js";
|
||||
import { scaleNumbers } from "./generic/units.js";
|
||||
import PlotGrid from "./generic/PlotGrid.svelte";
|
||||
@ -486,7 +488,7 @@
|
||||
quantities={$topUserQuery.data.topUser.map(
|
||||
(tu) => tu[topUserSelection.key],
|
||||
)}
|
||||
entities={$topUserQuery.data.topUser.map((tu) => tu.id)}
|
||||
entities={$topUserQuery.data.topUser.map((tu) => scrambleNames ? scramble(tu.id) : tu.id)}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
@ -520,14 +522,14 @@
|
||||
<th scope="col" id="topName-{tu.id}"
|
||||
><a
|
||||
href="/monitoring/user/{tu.id}?cluster={cluster}&state=running"
|
||||
>{tu.id}</a
|
||||
>{scrambleNames ? scramble(tu.id) : tu.id}</a
|
||||
></th
|
||||
>
|
||||
{#if tu?.name}
|
||||
<Tooltip
|
||||
target={`topName-${tu.id}`}
|
||||
placement="left"
|
||||
>{tu.name}</Tooltip
|
||||
>{scrambleNames ? scramble(tu.name) : tu.name}</Tooltip
|
||||
>
|
||||
{/if}
|
||||
<td>{tu[topUserSelection.key]}</td>
|
||||
@ -553,7 +555,7 @@
|
||||
quantities={$topProjectQuery.data.topProjects.map(
|
||||
(tp) => tp[topProjectSelection.key],
|
||||
)}
|
||||
entities={$topProjectQuery.data.topProjects.map((tp) => tp.id)}
|
||||
entities={$topProjectQuery.data.topProjects.map((tp) => scrambleNames ? scramble(tp.id) : tp.id)}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
@ -586,7 +588,7 @@
|
||||
<th scope="col"
|
||||
><a
|
||||
href="/monitoring/jobs/?cluster={cluster}&state=running&project={tp.id}&projectMatch=eq"
|
||||
>{tp.id}</a
|
||||
>{scrambleNames ? scramble(tp.id) : tp.id}</a
|
||||
></th
|
||||
>
|
||||
<td>{tp[topProjectSelection.key]}</td>
|
||||
|
110
web/frontend/src/Tags.root.svelte
Normal file
110
web/frontend/src/Tags.root.svelte
Normal file
@ -0,0 +1,110 @@
|
||||
<!--
|
||||
@component Tag List Svelte Component. Displays All Tags, Allows deletion.
|
||||
|
||||
Properties:
|
||||
- `username String!`: Users username.
|
||||
- `isAdmin Bool!`: User has Admin Auth.
|
||||
- `tagmap Object!`: Map of accessible, appwide tags. Prefiltered in backend.
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
gql,
|
||||
getContextClient,
|
||||
mutationStore,
|
||||
} from "@urql/svelte";
|
||||
import {
|
||||
Badge,
|
||||
InputGroup,
|
||||
Icon,
|
||||
Button,
|
||||
Spinner,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import {
|
||||
init,
|
||||
} from "./generic/utils.js";
|
||||
|
||||
export let username;
|
||||
export let isAdmin;
|
||||
export let tagmap;
|
||||
|
||||
const {} = init();
|
||||
const client = getContextClient();
|
||||
|
||||
let pendingChange = "none";
|
||||
|
||||
const removeTagMutation = ({ tagIds }) => {
|
||||
return mutationStore({
|
||||
client: client,
|
||||
query: gql`
|
||||
mutation ($tagIds: [ID!]!) {
|
||||
removeTagFromList(tagIds: $tagIds)
|
||||
}
|
||||
`,
|
||||
variables: { tagIds },
|
||||
});
|
||||
};
|
||||
|
||||
function removeTag(tag, tagType) {
|
||||
if (confirm("Are you sure you want to completely remove this tag?\n\n" + tagType + ':' + tag.name)) {
|
||||
pendingChange = tagType;
|
||||
removeTagMutation({tagIds: [tag.id] }).subscribe(
|
||||
(res) => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
tagmap[tagType] = tagmap[tagType].filter((t) => !res.data.removeTagFromList.includes(t.id));
|
||||
if (tagmap[tagType].length === 0) {
|
||||
delete tagmap[tagType]
|
||||
}
|
||||
pendingChange = "none";
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-10">
|
||||
{#each Object.entries(tagmap) as [tagType, tagList]}
|
||||
<div class="my-3 p-2 bg-secondary rounded text-white"> <!-- text-capitalize -->
|
||||
Tag Type: <b>{tagType}</b>
|
||||
{#if pendingChange === tagType}
|
||||
<Spinner size="sm" secondary />
|
||||
{/if}
|
||||
<span style="float: right; padding-bottom: 0.4rem; padding-top: 0.4rem;" class="badge bg-light text-secondary">
|
||||
{tagList.length} Tag{(tagList.length != 1)?'s':''}
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-inline-flex flex-wrap">
|
||||
{#each tagList as tag (tag.id)}
|
||||
<InputGroup class="w-auto flex-nowrap" style="margin-right: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<Button outline color="secondary" href="/monitoring/jobs/?tag={tag.id}" target="_blank">
|
||||
<Badge color="light" style="font-size:medium;" border>{tag.name}</Badge> :
|
||||
<Badge color="primary" pill>{tag.count} Job{(tag.count != 1)?'s':''}</Badge>
|
||||
{#if tag.scope == "global"}
|
||||
<Badge style="background-color:#c85fc8 !important;" pill>Global</Badge>
|
||||
{:else if tag.scope == "admin"}
|
||||
<Badge style="background-color:#19e5e6 !important;" pill>Admin</Badge>
|
||||
{:else}
|
||||
<Badge color="warning" pill>Private</Badge>
|
||||
{/if}
|
||||
</Button>
|
||||
{#if (isAdmin && (tag.scope == "admin" || tag.scope == "global")) || tag.scope == username }
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
on:click={() => removeTag(tag, tagType)}
|
||||
>
|
||||
<Icon name="x" />
|
||||
</Button>
|
||||
{/if}
|
||||
</InputGroup>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -14,7 +14,6 @@
|
||||
<script>
|
||||
import {
|
||||
getContext,
|
||||
createEventDispatcher
|
||||
} from "svelte";
|
||||
import {
|
||||
queryStore,
|
||||
@ -56,7 +55,6 @@
|
||||
let pendingZoomState = null;
|
||||
let thresholdState = null;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const statsPattern = /(.*)-stat$/;
|
||||
const unit = (metricUnit?.prefix ? metricUnit.prefix : "") + (metricUnit?.base ? metricUnit.base : "");
|
||||
const client = getContextClient();
|
||||
|
@ -17,6 +17,9 @@
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText, } from "@sveltestrap/sveltestrap";
|
||||
import {
|
||||
scramble,
|
||||
scrambleNames, } from "../../generic/utils.js";
|
||||
|
||||
export let cluster;
|
||||
export let subCluster
|
||||
@ -32,8 +35,8 @@
|
||||
let userList;
|
||||
let projectList;
|
||||
$: if (nodeJobsData) {
|
||||
userList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => j.user))).sort((a, b) => a.localeCompare(b));
|
||||
projectList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => j.project))).sort((a, b) => a.localeCompare(b));
|
||||
userList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => scrambleNames ? scramble(j.user) : j.user))).sort((a, b) => a.localeCompare(b));
|
||||
projectList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => scrambleNames ? scramble(j.project) : j.project))).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
getContextClient,
|
||||
} from "@urql/svelte";
|
||||
import { Card, CardBody, Spinner } from "@sveltestrap/sveltestrap";
|
||||
import { maxScope, checkMetricDisabled } from "../../generic/utils.js";
|
||||
import { maxScope, checkMetricDisabled, scramble, scrambleNames } from "../../generic/utils.js";
|
||||
import MetricPlot from "../../generic/plots/MetricPlot.svelte";
|
||||
import NodeInfo from "./NodeInfo.svelte";
|
||||
|
||||
@ -110,9 +110,12 @@
|
||||
extendedLegendData = {}
|
||||
for (const accId of accSet) {
|
||||
const matchJob = $nodeJobsData.data.jobs.items.find((i) => i.resources.find((r) => r.accelerators.includes(accId)))
|
||||
const matchUser = matchJob?.user ? matchJob.user : null
|
||||
extendedLegendData[accId] = {
|
||||
user: matchJob?.user ? matchJob?.user : '-',
|
||||
job: matchJob?.jobId ? matchJob?.jobId : '-',
|
||||
user: (scrambleNames && matchUser)
|
||||
? scramble(matchUser)
|
||||
: (matchUser ? matchUser : '-'),
|
||||
job: matchJob?.jobId ? matchJob.jobId : '-',
|
||||
}
|
||||
}
|
||||
// Theoretically extendable for hwthreadIDs
|
||||
|
16
web/frontend/src/tags.entrypoint.js
Normal file
16
web/frontend/src/tags.entrypoint.js
Normal file
@ -0,0 +1,16 @@
|
||||
import {} from './header.entrypoint.js'
|
||||
import Tags from './Tags.root.svelte'
|
||||
|
||||
new Tags({
|
||||
target: document.getElementById('svelte-app'),
|
||||
props: {
|
||||
username: username,
|
||||
isAdmin: isAdmin,
|
||||
tagmap: tagmap,
|
||||
},
|
||||
context: new Map([
|
||||
['cc-config', clusterCockpitConfig]
|
||||
])
|
||||
})
|
||||
|
||||
|
@ -1,37 +1,15 @@
|
||||
{{define "content"}}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-10">
|
||||
{{ range $tagType, $tagList := .Infos.tagmap }}
|
||||
<div class="my-3 p-2 bg-secondary rounded text-white"> <!-- text-capitalize -->
|
||||
Tag Type: <b>{{ $tagType }}</b>
|
||||
<span style="float: right; padding-bottom: 0.4rem; padding-top: 0.4rem;" class="badge bg-light text-secondary">
|
||||
{{len $tagList}} Tag{{if ne (len $tagList) 1}}s{{end}}
|
||||
</span>
|
||||
</div>
|
||||
{{ range $tagList }}
|
||||
{{if eq .scope "global"}}
|
||||
<a class="btn btn-outline-secondary" href="/monitoring/jobs/?tag={{ .id }}" role="button">
|
||||
{{ .name }}
|
||||
<span class="badge bg-primary mr-1">{{ .count }} Job{{if ne .count 1}}s{{end}}</span>
|
||||
<span style="background-color:#c85fc8;" class="badge text-dark">Global</span>
|
||||
</a>
|
||||
{{else if eq .scope "admin"}}
|
||||
<a class="btn btn-outline-secondary" href="/monitoring/jobs/?tag={{ .id }}" role="button">
|
||||
{{ .name }}
|
||||
<span class="badge bg-primary mr-1">{{ .count }} Job{{if ne .count 1}}s{{end}}</span>
|
||||
<span style="background-color:#19e5e6;" class="badge text-dark">Admin</span>
|
||||
</a>
|
||||
{{else}}
|
||||
<a class="btn btn-outline-secondary" href="/monitoring/jobs/?tag={{ .id }}" role="button">
|
||||
{{ .name }}
|
||||
<span class="badge bg-primary mr-1">{{ .count }} Job{{if ne .count 1}}s{{end}}</span>
|
||||
<span class="badge bg-warning text-dark">Private</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="svelte-app"></div>
|
||||
{{end}}
|
||||
{{define "stylesheets"}}
|
||||
<link rel='stylesheet' href='/build/taglist.css'>
|
||||
{{end}}
|
||||
{{define "javascript"}}
|
||||
<script>
|
||||
const username = {{ .User.Username }};
|
||||
const isAdmin = {{ .User.HasRole .Roles.admin }};
|
||||
const tagmap = {{ .Infos.tagmap }};
|
||||
const clusterCockpitConfig = {{ .Config }};
|
||||
</script>
|
||||
<script src='/build/taglist.js'></script>
|
||||
{{end}}
|
||||
|
Loading…
x
Reference in New Issue
Block a user