2024-04-11 23:04:30 +02:00
// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
2022-07-29 06:29:21 +02:00
// All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2021-12-16 09:35:03 +01:00
package api
import (
2022-01-17 13:27:40 +01:00
"bufio"
2022-02-07 14:56:46 +01:00
"database/sql"
2021-12-16 09:35:03 +01:00
"encoding/json"
2022-02-14 17:31:51 +01:00
"errors"
2021-12-16 09:35:03 +01:00
"fmt"
2022-01-07 09:39:00 +01:00
"io"
2021-12-16 09:35:03 +01:00
"net/http"
2022-01-07 09:39:00 +01:00
"os"
"path/filepath"
2022-02-07 09:57:06 +01:00
"strconv"
2022-02-22 10:51:58 +01:00
"strings"
2022-01-12 11:13:25 +01:00
"sync"
2022-02-22 10:51:58 +01:00
"time"
2021-12-16 09:35:03 +01:00
2024-08-28 11:13:54 +02:00
"github.com/ClusterCockpit/cc-backend/internal/archiver"
2022-06-21 17:52:36 +02:00
"github.com/ClusterCockpit/cc-backend/internal/auth"
2023-08-03 17:47:09 +02:00
"github.com/ClusterCockpit/cc-backend/internal/config"
2022-06-21 17:52:36 +02:00
"github.com/ClusterCockpit/cc-backend/internal/graph"
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
2023-04-28 08:49:58 +02:00
"github.com/ClusterCockpit/cc-backend/internal/importer"
2024-08-28 10:03:04 +02:00
"github.com/ClusterCockpit/cc-backend/internal/metricDataDispatcher"
2022-06-21 17:52:36 +02:00
"github.com/ClusterCockpit/cc-backend/internal/repository"
2023-08-14 14:33:05 +02:00
"github.com/ClusterCockpit/cc-backend/internal/util"
2022-09-05 17:46:38 +02:00
"github.com/ClusterCockpit/cc-backend/pkg/archive"
2022-06-21 17:52:36 +02:00
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
2021-12-16 09:35:03 +01:00
"github.com/gorilla/mux"
)
2022-09-15 12:37:44 +02:00
// @title ClusterCockpit REST API
2023-06-15 09:55:24 +02:00
// @version 1.0.0
2022-09-15 12:37:44 +02:00
// @description API for batch job control.
2022-11-11 15:26:27 +01:00
2022-09-21 11:54:19 +02:00
// @contact.name ClusterCockpit Project
// @contact.url https://github.com/ClusterCockpit
2022-09-16 06:09:55 +02:00
// @contact.email support@clustercockpit.org
2022-09-21 11:54:19 +02:00
2022-09-15 12:37:44 +02:00
// @license.name MIT License
// @license.url https://opensource.org/licenses/MIT
2022-09-21 11:54:19 +02:00
2022-11-11 15:26:27 +01:00
// @host localhost:8080
// @basePath /api
2022-09-21 11:54:19 +02:00
2022-09-15 12:37:44 +02:00
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name X-Auth-Token
2021-12-16 09:35:03 +01:00
type RestApi struct {
2023-02-17 15:45:31 +01:00
JobRepository * repository . JobRepository
Authentication * auth . Authentication
MachineStateDir string
RepositoryMutex sync . Mutex
2021-12-16 09:35:03 +01:00
}
2024-07-16 12:08:10 +02:00
func New ( ) * RestApi {
return & RestApi {
JobRepository : repository . GetJobRepository ( ) ,
MachineStateDir : config . Keys . MachineStateDir ,
Authentication : auth . GetAuthInstance ( ) ,
}
}
2024-07-03 17:24:26 +02:00
func ( api * RestApi ) MountApiRoutes ( r * mux . Router ) {
2022-01-10 16:14:54 +01:00
r . StrictSlash ( true )
2021-12-16 09:35:03 +01:00
2022-01-10 16:14:54 +01:00
r . HandleFunc ( "/jobs/start_job/" , api . startJob ) . Methods ( http . MethodPost , http . MethodPut )
2022-09-15 12:37:44 +02:00
r . HandleFunc ( "/jobs/stop_job/" , api . stopJobByRequest ) . Methods ( http . MethodPost , http . MethodPut )
2022-09-05 17:46:38 +02:00
// r.HandleFunc("/jobs/import/", api.importJob).Methods(http.MethodPost, http.MethodPut)
2022-01-07 09:39:00 +01:00
2022-01-17 13:27:40 +01:00
r . HandleFunc ( "/jobs/" , api . getJobs ) . Methods ( http . MethodGet )
2023-06-14 15:03:01 +02:00
r . HandleFunc ( "/jobs/{id}" , api . getJobById ) . Methods ( http . MethodPost )
2024-03-08 15:31:34 +01:00
r . HandleFunc ( "/jobs/{id}" , api . getCompleteJobById ) . Methods ( http . MethodGet )
2022-01-10 16:14:54 +01:00
r . HandleFunc ( "/jobs/tag_job/{id}" , api . tagJob ) . Methods ( http . MethodPost , http . MethodPatch )
2024-03-08 08:51:05 +01:00
r . HandleFunc ( "/jobs/edit_meta/{id}" , api . editMeta ) . Methods ( http . MethodPost , http . MethodPatch )
2022-01-27 09:29:53 +01:00
r . HandleFunc ( "/jobs/metrics/{id}" , api . getJobMetrics ) . Methods ( http . MethodGet )
2022-11-11 15:26:27 +01:00
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 )
2022-03-02 10:50:08 +01:00
2024-03-08 16:35:30 +01:00
r . HandleFunc ( "/clusters/" , api . getClusters ) . Methods ( http . MethodGet )
2022-01-17 13:27:40 +01:00
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 )
}
2024-07-03 17:24:26 +02:00
}
func ( api * RestApi ) MountUserApiRoutes ( r * mux . Router ) {
r . StrictSlash ( true )
r . HandleFunc ( "/jobs/" , api . getJobs ) . Methods ( http . MethodGet )
r . HandleFunc ( "/jobs/{id}" , api . getJobById ) . Methods ( http . MethodPost )
r . HandleFunc ( "/jobs/{id}" , api . getCompleteJobById ) . Methods ( http . MethodGet )
r . HandleFunc ( "/jobs/metrics/{id}" , api . getJobMetrics ) . Methods ( http . MethodGet )
}
func ( api * RestApi ) MountConfigApiRoutes ( r * mux . Router ) {
r . StrictSlash ( true )
2023-08-14 14:33:05 +02:00
if api . Authentication != nil {
2023-08-14 18:38:30 +02:00
r . HandleFunc ( "/roles/" , api . getRoles ) . Methods ( http . MethodGet )
r . HandleFunc ( "/users/" , api . createUser ) . Methods ( http . MethodPost , http . MethodPut )
r . HandleFunc ( "/users/" , api . getUsers ) . Methods ( http . MethodGet )
r . HandleFunc ( "/users/" , api . deleteUser ) . Methods ( http . MethodDelete )
r . HandleFunc ( "/user/{id}" , api . updateUser ) . Methods ( http . MethodPost )
2024-07-04 11:16:45 +02:00
}
}
2024-07-05 11:48:06 +02:00
func ( api * RestApi ) MountFrontendApiRoutes ( r * mux . Router ) {
2024-07-04 11:16:45 +02:00
r . StrictSlash ( true )
if api . Authentication != nil {
2024-07-05 11:48:06 +02:00
r . HandleFunc ( "/jwt/" , api . getJWT ) . Methods ( http . MethodGet )
2023-08-14 18:38:30 +02:00
r . HandleFunc ( "/configuration/" , api . updateConfiguration ) . Methods ( http . MethodPost )
2023-08-14 14:33:05 +02:00
}
2021-12-16 09:35:03 +01:00
}
2022-09-15 12:37:44 +02:00
// StartJobApiResponse model
2022-02-06 09:48:31 +01:00
type StartJobApiResponse struct {
2024-11-26 07:02:53 +01:00
Message string ` json:"msg" `
2021-12-16 09:35:03 +01:00
}
2022-11-11 15:26:27 +01:00
// DeleteJobApiResponse model
type DeleteJobApiResponse struct {
Message string ` json:"msg" `
}
2023-08-03 17:47:09 +02:00
// UpdateUserApiResponse model
type UpdateUserApiResponse struct {
Message string ` json:"msg" `
}
2022-09-15 12:37:44 +02:00
// StopJobApiRequest model
2021-12-16 09:35:03 +01:00
type StopJobApiRequest struct {
2024-03-19 16:18:43 +01:00
JobId * int64 ` json:"jobId" example:"123000" `
Cluster * string ` json:"cluster" example:"fritz" `
StartTime * int64 ` json:"startTime" example:"1649723812" `
State schema . JobState ` json:"jobState" validate:"required" example:"completed" `
2022-09-21 11:54:19 +02:00
StopTime int64 ` json:"stopTime" validate:"required" example:"1649763839" `
2021-12-16 09:35:03 +01:00
}
2022-11-11 15:26:27 +01:00
// DeleteJobApiRequest model
type DeleteJobApiRequest struct {
JobId * int64 ` json:"jobId" validate:"required" example:"123000" ` // Cluster Job ID of job
Cluster * string ` json:"cluster" example:"fritz" ` // Cluster of job
StartTime * int64 ` json:"startTime" example:"1649723812" ` // Start Time of job as epoch
}
2023-02-25 07:21:54 +01:00
// GetJobsApiResponse model
type GetJobsApiResponse struct {
Jobs [ ] * schema . JobMeta ` json:"jobs" ` // Array of jobs
Items int ` json:"items" ` // Number of jobs returned
Page int ` json:"page" ` // Page id returned
}
2024-03-08 16:35:30 +01:00
// GetClustersApiResponse model
type GetClustersApiResponse struct {
Clusters [ ] * schema . Cluster ` json:"clusters" ` // Array of clusters
}
2022-09-15 12:37:44 +02:00
// ErrorResponse model
2022-02-14 17:31:51 +01:00
type ErrorResponse struct {
2022-09-15 12:37:44 +02:00
// Statustext of Errorcode
2022-02-14 17:31:51 +01:00
Status string ` json:"status" `
2022-09-15 12:37:44 +02:00
Error string ` json:"error" ` // Error Message
}
2022-11-11 15:26:27 +01:00
// ApiTag model
type ApiTag struct {
2022-09-16 13:18:50 +02:00
// Tag Type
2024-08-01 18:59:24 +02:00
Type string ` json:"type" example:"Debug" `
Name string ` json:"name" example:"Testjob" ` // Tag Name
Scope string ` json:"scope" example:"global" ` // Tag Scope for Frontend Display
2022-02-14 17:31:51 +01:00
}
2024-03-08 08:51:05 +01:00
// ApiMeta model
type EditMetaRequest struct {
Key string ` json:"key" example:"jobScript" `
Value string ` json:"value" example:"bash script" `
}
2022-11-11 15:26:27 +01:00
type TagJobApiRequest [ ] * ApiTag
2022-09-16 11:21:27 +02:00
2023-06-14 14:33:36 +02:00
type GetJobApiRequest [ ] string
type GetJobApiResponse struct {
Meta * schema . Job
2023-06-14 15:03:01 +02:00
Data [ ] * JobMetricWithName
}
2024-03-08 15:31:34 +01:00
type GetCompleteJobApiResponse struct {
Meta * schema . Job
Data schema . JobData
}
2023-06-14 15:03:01 +02:00
type JobMetricWithName struct {
2024-03-19 16:18:43 +01:00
Metric * schema . JobMetric ` json:"metric" `
2023-06-14 15:03:01 +02:00
Name string ` json:"name" `
Scope schema . MetricScope ` json:"scope" `
2023-06-14 14:33:36 +02:00
}
2023-08-21 12:12:28 +02:00
type ApiReturnedUser struct {
Username string ` json:"username" `
Name string ` json:"name" `
Roles [ ] string ` json:"roles" `
Email string ` json:"email" `
Projects [ ] string ` json:"projects" `
}
2022-02-14 17:31:51 +01:00
func handleError ( err error , statusCode int , rw http . ResponseWriter ) {
2023-01-23 18:48:06 +01:00
log . Warnf ( "REST ERROR : %s" , err . Error ( ) )
2022-02-14 17:31:51 +01:00
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
rw . WriteHeader ( statusCode )
json . NewEncoder ( rw ) . Encode ( ErrorResponse {
Status : http . StatusText ( statusCode ) ,
Error : err . Error ( ) ,
} )
}
2022-02-22 10:51:58 +01:00
func decode ( r io . Reader , val interface { } ) error {
dec := json . NewDecoder ( r )
dec . DisallowUnknownFields ( )
return dec . Decode ( val )
}
2023-08-14 18:38:30 +02:00
func securedCheck ( r * http . Request ) error {
2023-08-17 10:29:00 +02:00
user := repository . GetUserFromContext ( r . Context ( ) )
2023-08-14 18:38:30 +02:00
if user == nil {
return fmt . Errorf ( "no user in context" )
}
2023-08-17 10:29:00 +02:00
if user . AuthType == schema . AuthToken {
2023-08-14 18:38:30 +02:00
// If nothing declared in config: deny all request to this endpoint
if config . Keys . ApiAllowedIPs == nil || len ( config . Keys . ApiAllowedIPs ) == 0 {
return fmt . Errorf ( "missing configuration key ApiAllowedIPs" )
}
2023-08-18 13:03:11 +02:00
if config . Keys . ApiAllowedIPs [ 0 ] == "*" {
return nil
}
2023-08-14 18:38:30 +02:00
// extract IP address
IPAddress := r . Header . Get ( "X-Real-Ip" )
if IPAddress == "" {
IPAddress = r . Header . Get ( "X-Forwarded-For" )
}
if IPAddress == "" {
IPAddress = r . RemoteAddr
}
2023-09-07 16:36:47 +02:00
if strings . Contains ( IPAddress , ":" ) {
IPAddress = strings . Split ( IPAddress , ":" ) [ 0 ]
}
2023-08-14 18:38:30 +02:00
// check if IP is allowed
if ! util . Contains ( config . Keys . ApiAllowedIPs , IPAddress ) {
return fmt . Errorf ( "unknown ip: %v" , IPAddress )
}
}
return nil
}
2024-03-08 16:35:30 +01:00
// getClusters godoc
// @summary Lists all cluster configs
// @tags Cluster query
// @description Get a list of all cluster configs. Specific cluster can be requested using query parameter.
// @produce json
// @param cluster query string false "Job Cluster"
// @success 200 {object} api.GetClustersApiResponse "Array of clusters"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /clusters/ [get]
func ( api * RestApi ) getClusters ( rw http . ResponseWriter , r * http . Request ) {
if user := repository . GetUserFromContext ( r . Context ( ) ) ; user != nil &&
! user . HasRole ( schema . RoleApi ) {
handleError ( fmt . Errorf ( "missing role: %v" , schema . GetRoleString ( schema . RoleApi ) ) , http . StatusForbidden , rw )
return
}
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
bw := bufio . NewWriter ( rw )
defer bw . Flush ( )
var clusters [ ] * schema . Cluster
if r . URL . Query ( ) . Has ( "cluster" ) {
name := r . URL . Query ( ) . Get ( "cluster" )
cluster := archive . GetCluster ( name )
if cluster == nil {
handleError ( fmt . Errorf ( "unknown cluster: %s" , name ) , http . StatusBadRequest , rw )
return
}
clusters = append ( clusters , cluster )
} else {
clusters = archive . Clusters
}
payload := GetClustersApiResponse {
Clusters : clusters ,
}
if err := json . NewEncoder ( bw ) . Encode ( payload ) ; err != nil {
handleError ( err , http . StatusInternalServerError , rw )
return
}
}
2022-09-15 12:37:44 +02:00
// getJobs godoc
2022-11-11 15:26:27 +01:00
// @summary Lists all jobs
2023-09-07 15:14:09 +02:00
// @tags Job query
2022-11-11 15:26:27 +01:00
// @description Get a list of all jobs. Filters can be applied using query parameters.
// @description Number of results can be limited by page. Results are sorted by descending startTime.
// @produce json
// @param state query string false "Job State" Enums(running, completed, failed, cancelled, stopped, timeout)
// @param cluster query string false "Job Cluster"
// @param start-time query string false "Syntax: '$from-$to', as unix epoch timestamps in seconds"
// @param items-per-page query int false "Items per page (Default: 25)"
// @param page query int false "Page Number (Default: 1)"
// @param with-metadata query bool false "Include metadata (e.g. jobScript) in response"
2023-02-25 07:21:54 +01:00
// @success 200 {object} api.GetJobsApiResponse "Job array and page info"
2022-11-11 15:26:27 +01:00
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
2023-02-27 09:16:18 +01:00
// @failure 403 {object} api.ErrorResponse "Forbidden"
2022-11-11 15:26:27 +01:00
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/ [get]
2022-01-17 13:27:40 +01:00
func ( api * RestApi ) getJobs ( rw http . ResponseWriter , r * http . Request ) {
2022-03-15 09:49:41 +01:00
withMetadata := false
2022-02-22 10:51:58 +01:00
filter := & model . JobFilter { }
2022-11-11 15:26:27 +01:00
page := & model . PageRequest { ItemsPerPage : 25 , Page : 1 }
2022-02-22 10:51:58 +01:00
order := & model . OrderByInput { Field : "startTime" , Order : model . SortDirectionEnumDesc }
2022-11-11 15:26:27 +01:00
2022-01-17 13:27:40 +01:00
for key , vals := range r . URL . Query ( ) {
switch key {
case "state" :
for _ , s := range vals {
state := schema . JobState ( s )
if ! state . Valid ( ) {
2023-02-25 07:21:54 +01:00
handleError ( fmt . Errorf ( "invalid query parameter value: state" ) ,
http . StatusBadRequest , rw )
2022-01-17 13:27:40 +01:00
return
}
filter . State = append ( filter . State , state )
}
case "cluster" :
filter . Cluster = & model . StringInput { Eq : & vals [ 0 ] }
2022-02-22 10:51:58 +01:00
case "start-time" :
st := strings . Split ( vals [ 0 ] , "-" )
if len ( st ) != 2 {
2023-02-25 07:21:54 +01:00
handleError ( fmt . Errorf ( "invalid query parameter value: startTime" ) ,
http . StatusBadRequest , rw )
2022-02-22 10:51:58 +01:00
return
}
from , err := strconv . ParseInt ( st [ 0 ] , 10 , 64 )
if err != nil {
2023-02-25 07:21:54 +01:00
handleError ( err , http . StatusBadRequest , rw )
2022-02-22 10:51:58 +01:00
return
}
to , err := strconv . ParseInt ( st [ 1 ] , 10 , 64 )
if err != nil {
2023-02-25 07:21:54 +01:00
handleError ( err , http . StatusBadRequest , rw )
2022-02-22 10:51:58 +01:00
return
}
ufrom , uto := time . Unix ( from , 0 ) , time . Unix ( to , 0 )
2022-09-07 12:24:45 +02:00
filter . StartTime = & schema . TimeRange { From : & ufrom , To : & uto }
2022-02-22 10:51:58 +01:00
case "page" :
x , err := strconv . Atoi ( vals [ 0 ] )
if err != nil {
2023-02-25 07:21:54 +01:00
handleError ( err , http . StatusBadRequest , rw )
2022-02-22 10:51:58 +01:00
return
}
page . Page = x
case "items-per-page" :
x , err := strconv . Atoi ( vals [ 0 ] )
if err != nil {
2023-02-25 07:21:54 +01:00
handleError ( err , http . StatusBadRequest , rw )
2022-02-22 10:51:58 +01:00
return
}
page . ItemsPerPage = x
2022-03-15 09:49:41 +01:00
case "with-metadata" :
withMetadata = true
2022-01-17 13:27:40 +01:00
default :
2023-02-25 07:21:54 +01:00
handleError ( fmt . Errorf ( "invalid query parameter: %s" , key ) ,
http . StatusBadRequest , rw )
2022-01-17 13:27:40 +01:00
return
}
}
2022-02-22 10:51:58 +01:00
jobs , err := api . JobRepository . QueryJobs ( r . Context ( ) , [ ] * model . JobFilter { filter } , page , order )
2022-01-17 13:27:40 +01:00
if err != nil {
2023-02-25 07:21:54 +01:00
handleError ( err , http . StatusInternalServerError , rw )
2022-01-17 13:27:40 +01:00
return
}
2022-02-22 10:51:58 +01:00
results := make ( [ ] * schema . JobMeta , 0 , len ( jobs ) )
for _ , job := range jobs {
2022-03-15 09:49:41 +01:00
if withMetadata {
2023-05-04 07:00:30 +02:00
if _ , err = api . JobRepository . FetchMetadata ( job ) ; err != nil {
2023-02-25 07:21:54 +01:00
handleError ( err , http . StatusInternalServerError , rw )
2022-03-15 09:49:41 +01:00
return
}
}
2022-02-22 10:51:58 +01:00
res := & schema . JobMeta {
ID : & job . ID ,
BaseJob : job . BaseJob ,
StartTime : job . StartTime . Unix ( ) ,
}
2022-01-17 13:27:40 +01:00
2024-11-25 16:44:50 +01:00
res . Tags , err = api . JobRepository . GetTags ( repository . GetUserFromContext ( r . Context ( ) ) , & job . ID )
2022-02-22 10:51:58 +01:00
if err != nil {
2023-02-25 07:21:54 +01:00
handleError ( err , http . StatusInternalServerError , rw )
2022-02-22 10:51:58 +01:00
return
}
2022-01-17 13:27:40 +01:00
2022-02-22 10:51:58 +01:00
if res . MonitoringStatus == schema . MonitoringStatusArchivingSuccessful {
2022-09-05 17:46:38 +02:00
res . Statistics , err = archive . GetStatistics ( job )
2022-02-22 10:51:58 +01:00
if err != nil {
2024-03-08 16:35:30 +01:00
handleError ( err , http . StatusInternalServerError , rw )
return
2022-02-22 10:51:58 +01:00
}
}
2021-12-16 09:35:03 +01:00
2022-02-22 10:51:58 +01:00
results = append ( results , res )
2021-12-16 09:35:03 +01:00
}
2022-02-22 10:51:58 +01:00
log . Debugf ( "/api/jobs: %d jobs returned" , len ( results ) )
2023-02-25 07:21:54 +01:00
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
2022-02-22 10:51:58 +01:00
bw := bufio . NewWriter ( rw )
defer bw . Flush ( )
2023-02-25 07:21:54 +01:00
payload := GetJobsApiResponse {
Jobs : results ,
Items : page . ItemsPerPage ,
Page : page . Page ,
}
if err := json . NewEncoder ( bw ) . Encode ( payload ) ; err != nil {
2023-06-14 14:33:36 +02:00
handleError ( err , http . StatusInternalServerError , rw )
return
}
}
2024-07-03 17:24:26 +02:00
// getCompleteJobById godoc
2024-03-08 15:31:34 +01:00
// @summary Get job meta and optional all metric data
// @tags Job query
// @description Job to get is specified by database ID
// @description Returns full job resource information according to 'JobMeta' scheme and all metrics according to 'JobData'.
// @produce json
// @param id path int true "Database ID of Job"
// @param all-metrics query bool false "Include all available metrics"
// @success 200 {object} api.GetJobApiResponse "Job resource"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/{id} [get]
func ( api * RestApi ) getCompleteJobById ( rw http . ResponseWriter , r * http . Request ) {
// Fetch job from db
id , ok := mux . Vars ( r ) [ "id" ]
var job * schema . Job
var err error
if ok {
id , e := strconv . ParseInt ( id , 10 , 64 )
if e != nil {
handleError ( fmt . Errorf ( "integer expected in path for id: %w" , e ) , http . StatusBadRequest , rw )
return
}
2024-07-03 17:24:26 +02:00
job , err = api . JobRepository . FindById ( r . Context ( ) , id ) // Get Job from Repo by ID
2024-03-08 15:31:34 +01:00
} else {
2024-10-25 15:23:49 +02:00
handleError ( fmt . Errorf ( "the parameter 'id' is required" ) , http . StatusBadRequest , rw )
2024-03-08 15:31:34 +01:00
return
}
if err != nil {
2024-10-25 15:23:49 +02:00
handleError ( fmt . Errorf ( "finding job with db id %s failed: %w" , id , err ) , http . StatusUnprocessableEntity , rw )
2024-03-08 15:31:34 +01:00
return
}
2024-11-25 16:44:50 +01:00
job . Tags , err = api . JobRepository . GetTags ( repository . GetUserFromContext ( r . Context ( ) ) , & job . ID )
2024-03-19 16:18:43 +01:00
if err != nil {
handleError ( err , http . StatusInternalServerError , rw )
return
}
if _ , err = api . JobRepository . FetchMetadata ( job ) ; err != nil {
handleError ( err , http . StatusInternalServerError , rw )
return
}
2024-03-08 15:31:34 +01:00
var scopes [ ] schema . MetricScope
if job . NumNodes == 1 {
scopes = [ ] schema . MetricScope { "core" }
} else {
scopes = [ ] schema . MetricScope { "node" }
}
var data schema . JobData
2024-08-22 14:29:51 +02:00
metricConfigs := archive . GetCluster ( job . Cluster ) . MetricConfig
resolution := 0
for _ , mc := range metricConfigs {
resolution = max ( resolution , mc . Timestep )
}
2024-03-19 16:18:43 +01:00
if r . URL . Query ( ) . Get ( "all-metrics" ) == "true" {
2024-09-05 17:26:43 +02:00
data , err = metricDataDispatcher . LoadData ( job , nil , scopes , r . Context ( ) , resolution )
2024-03-08 15:31:34 +01:00
if err != nil {
2024-10-25 15:23:49 +02:00
log . Warnf ( "REST: error while loading all-metrics job data for JobID %d on %s" , job . JobID , job . Cluster )
2024-03-08 15:31:34 +01:00
return
}
}
log . Debugf ( "/api/job/%s: get job %d" , id , job . JobID )
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
bw := bufio . NewWriter ( rw )
defer bw . Flush ( )
payload := GetCompleteJobApiResponse {
Meta : job ,
Data : data ,
}
if err := json . NewEncoder ( bw ) . Encode ( payload ) ; err != nil {
handleError ( err , http . StatusInternalServerError , rw )
return
}
}
// getJobById godoc
// @summary Get job meta and configurable metric data
2023-09-07 15:14:09 +02:00
// @tags Job query
2023-06-14 14:33:36 +02:00
// @description Job to get is specified by database ID
// @description Returns full job resource information according to 'JobMeta' scheme and all metrics according to 'JobData'.
// @accept json
// @produce json
2024-03-08 15:31:34 +01:00
// @param id path int true "Database ID of Job"
// @param request body api.GetJobApiRequest true "Array of metric names"
2023-06-14 15:03:01 +02:00
// @success 200 {object} api.GetJobApiResponse "Job resource"
2023-06-14 14:33:36 +02:00
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
2023-06-14 15:03:01 +02:00
// @router /jobs/{id} [post]
2023-06-14 14:33:36 +02:00
func ( api * RestApi ) getJobById ( rw http . ResponseWriter , r * http . Request ) {
// Fetch job from db
id , ok := mux . Vars ( r ) [ "id" ]
var job * schema . Job
var err error
if ok {
id , e := strconv . ParseInt ( id , 10 , 64 )
if e != nil {
handleError ( fmt . Errorf ( "integer expected in path for id: %w" , e ) , http . StatusBadRequest , rw )
return
}
2024-07-03 17:24:26 +02:00
job , err = api . JobRepository . FindById ( r . Context ( ) , id )
2023-06-14 14:33:36 +02:00
} else {
handleError ( errors . New ( "the parameter 'id' is required" ) , http . StatusBadRequest , rw )
return
}
if err != nil {
2024-10-25 15:23:49 +02:00
handleError ( fmt . Errorf ( "finding job with db id %s failed: %w" , id , err ) , http . StatusUnprocessableEntity , rw )
2023-06-14 14:33:36 +02:00
return
}
2024-11-25 16:44:50 +01:00
job . Tags , err = api . JobRepository . GetTags ( repository . GetUserFromContext ( r . Context ( ) ) , & job . ID )
2024-03-19 16:18:43 +01:00
if err != nil {
handleError ( err , http . StatusInternalServerError , rw )
return
}
if _ , err = api . JobRepository . FetchMetadata ( job ) ; err != nil {
handleError ( err , http . StatusInternalServerError , rw )
return
}
2023-06-14 14:33:36 +02:00
var metrics GetJobApiRequest
2023-06-14 15:03:01 +02:00
if err = decode ( r . Body , & metrics ) ; err != nil {
2023-06-14 14:33:36 +02:00
http . Error ( rw , err . Error ( ) , http . StatusBadRequest )
return
}
var scopes [ ] schema . MetricScope
if job . NumNodes == 1 {
scopes = [ ] schema . MetricScope { "core" }
} else {
scopes = [ ] schema . MetricScope { "node" }
}
2024-08-22 14:29:51 +02:00
metricConfigs := archive . GetCluster ( job . Cluster ) . MetricConfig
resolution := 0
for _ , mc := range metricConfigs {
resolution = max ( resolution , mc . Timestep )
}
2024-09-05 17:26:43 +02:00
data , err := metricDataDispatcher . LoadData ( job , metrics , scopes , r . Context ( ) , resolution )
2023-06-14 14:33:36 +02:00
if err != nil {
2024-10-25 15:23:49 +02:00
log . Warnf ( "REST: error while loading job data for JobID %d on %s" , job . JobID , job . Cluster )
2023-06-14 14:33:36 +02:00
return
}
2023-06-14 15:03:01 +02:00
res := [ ] * JobMetricWithName { }
2023-06-14 14:33:36 +02:00
for name , md := range data {
for scope , metric := range md {
2023-06-14 15:03:01 +02:00
res = append ( res , & JobMetricWithName {
2023-06-14 14:33:36 +02:00
Name : name ,
Scope : scope ,
Metric : metric ,
} )
}
}
2023-06-14 15:03:01 +02:00
log . Debugf ( "/api/job/%s: get job %d" , id , job . JobID )
2023-06-14 14:33:36 +02:00
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
bw := bufio . NewWriter ( rw )
defer bw . Flush ( )
payload := GetJobApiResponse {
Meta : job ,
Data : res ,
}
if err := json . NewEncoder ( bw ) . Encode ( payload ) ; err != nil {
2023-02-25 07:21:54 +01:00
handleError ( err , http . StatusInternalServerError , rw )
2021-12-16 09:35:03 +01:00
return
}
}
2024-03-08 08:51:05 +01:00
// editMeta godoc
// @summary Edit meta-data json
// @tags Job add and modify
// @description Edit key value pairs in job metadata json
// @description If a key already exists its content will be overwritten
// @accept json
// @produce json
// @param id path int true "Job Database ID"
// @param request body api.EditMetaRequest true "Kay value pair to add"
// @success 200 {object} schema.Job "Updated job resource"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 404 {object} api.ErrorResponse "Job does not exist"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/edit_meta/{id} [post]
func ( api * RestApi ) editMeta ( rw http . ResponseWriter , r * http . Request ) {
2024-07-03 17:24:26 +02:00
id , err := strconv . ParseInt ( mux . Vars ( r ) [ "id" ] , 10 , 64 )
2024-03-08 08:51:05 +01:00
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusBadRequest )
return
}
2024-07-03 17:24:26 +02:00
job , err := api . JobRepository . FindById ( r . Context ( ) , id )
2024-03-08 08:51:05 +01:00
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusNotFound )
return
}
var req EditMetaRequest
if err := decode ( r . Body , & req ) ; err != nil {
http . Error ( rw , err . Error ( ) , http . StatusBadRequest )
return
}
if err := api . JobRepository . UpdateMetadata ( job , req . Key , req . Value ) ; err != nil {
http . Error ( rw , err . Error ( ) , http . StatusInternalServerError )
return
}
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusOK )
json . NewEncoder ( rw ) . Encode ( job )
}
2022-09-15 12:37:44 +02:00
// tagJob godoc
2022-11-11 15:26:27 +01:00
// @summary Adds one or more tags to a job
2023-09-07 15:14:09 +02:00
// @tags Job add and modify
2022-11-11 15:26:27 +01:00
// @description Adds tag(s) to a job specified by DB ID. Name and Type of Tag(s) can be chosen freely.
2024-09-19 15:21:32 +02:00
// @description Tag Scope for frontend visibility will default to "global" if none entered, other options: "admin" or specific username.
2022-11-11 15:26:27 +01:00
// @description If tagged job is already finished: Tag will be written directly to respective archive files.
// @accept json
// @produce json
// @param id path int true "Job Database ID"
// @param request body api.TagJobApiRequest true "Array of tag-objects to add"
// @success 200 {object} schema.Job "Updated job resource"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 404 {object} api.ErrorResponse "Job or tag does not exist"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/tag_job/{id} [post]
2021-12-16 09:35:03 +01:00
func ( api * RestApi ) tagJob ( rw http . ResponseWriter , r * http . Request ) {
2024-07-03 17:24:26 +02:00
id , err := strconv . ParseInt ( mux . Vars ( r ) [ "id" ] , 10 , 64 )
2022-02-22 10:51:58 +01:00
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusBadRequest )
return
}
2024-07-03 17:24:26 +02:00
job , err := api . JobRepository . FindById ( r . Context ( ) , id )
2021-12-16 09:35:03 +01:00
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusNotFound )
return
}
2024-11-25 16:44:50 +01:00
job . Tags , err = api . JobRepository . GetTags ( repository . GetUserFromContext ( r . Context ( ) ) , & job . ID )
2021-12-16 09:35:03 +01:00
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusInternalServerError )
return
}
var req TagJobApiRequest
2022-02-22 10:51:58 +01:00
if err := decode ( r . Body , & req ) ; err != nil {
2021-12-16 09:35:03 +01:00
http . Error ( rw , err . Error ( ) , http . StatusBadRequest )
return
}
for _ , tag := range req {
2024-11-25 16:44:50 +01:00
tagId , err := api . JobRepository . AddTagOrCreate ( repository . GetUserFromContext ( r . Context ( ) ) , job . ID , tag . Type , tag . Name , tag . Scope )
2022-02-08 12:49:28 +01:00
if err != nil {
2021-12-16 09:35:03 +01:00
http . Error ( rw , err . Error ( ) , http . StatusInternalServerError )
return
}
2021-12-17 15:49:22 +01:00
job . Tags = append ( job . Tags , & schema . Tag {
2024-08-01 18:59:24 +02:00
ID : tagId ,
Type : tag . Type ,
Name : tag . Name ,
Scope : tag . Scope ,
2021-12-16 09:35:03 +01:00
} )
}
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusOK )
json . NewEncoder ( rw ) . Encode ( job )
}
2022-09-15 12:37:44 +02:00
// startJob godoc
2022-11-11 15:26:27 +01:00
// @summary Adds a new job as "running"
2023-09-07 15:14:09 +02:00
// @tags Job add and modify
2022-11-11 15:26:27 +01:00
// @description Job specified in request body will be saved to database as "running" with new DB ID.
// @description Job specifications follow the 'JobMeta' scheme, API will fail to execute if requirements are not met.
// @accept json
// @produce json
// @param request body schema.JobMeta true "Job to add"
// @success 201 {object} api.StartJobApiResponse "Job added successfully"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: The combination of jobId, clusterId and startTime does already exist"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/start_job/ [post]
2021-12-16 09:35:03 +01:00
func ( api * RestApi ) startJob ( rw http . ResponseWriter , r * http . Request ) {
2021-12-17 15:49:22 +01:00
req := schema . JobMeta { BaseJob : schema . JobDefaults }
2022-02-22 10:51:58 +01:00
if err := decode ( r . Body , & req ) ; err != nil {
2022-02-14 17:40:47 +01:00
handleError ( fmt . Errorf ( "parsing request body failed: %w" , err ) , http . StatusBadRequest , rw )
2021-12-16 09:35:03 +01:00
return
}
2022-02-25 10:50:43 +01:00
if req . State == "" {
req . State = schema . JobStateRunning
2021-12-16 09:35:03 +01:00
}
2023-04-28 08:49:58 +02:00
if err := importer . SanityChecks ( & req . BaseJob ) ; err != nil {
2022-02-25 10:50:43 +01:00
handleError ( err , http . StatusBadRequest , rw )
2021-12-16 09:35:03 +01:00
return
}
// Check if combination of (job_id, cluster_id, start_time) already exists:
2022-11-09 11:49:36 +01:00
jobs , err := api . JobRepository . FindAll ( & req . JobID , & req . Cluster , nil )
2022-03-21 13:04:57 +01:00
if err != nil && err != sql . ErrNoRows {
2022-02-14 17:40:47 +01:00
handleError ( fmt . Errorf ( "checking for duplicate failed: %w" , err ) , http . StatusInternalServerError , rw )
2021-12-16 09:35:03 +01:00
return
2022-03-21 13:04:57 +01:00
} else if err == nil {
2022-11-09 11:49:36 +01:00
for _ , job := range jobs {
if ( req . StartTime - job . StartTimeUnix ) < 86400 {
2023-06-20 15:47:38 +02:00
handleError ( fmt . Errorf ( "a job with that jobId, cluster and startTime already exists: dbid: %d, jobid: %d" , job . ID , job . JobID ) , http . StatusUnprocessableEntity , rw )
2022-11-09 11:49:36 +01:00
return
}
2022-03-21 11:28:59 +01:00
}
2021-12-16 09:35:03 +01:00
}
2024-11-25 16:44:50 +01:00
repository . TriggerJobStart ( repository . JobWithUser { Job : & req , User : repository . GetUserFromContext ( r . Context ( ) ) } )
2021-12-16 09:35:03 +01:00
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusCreated )
2022-02-06 09:48:31 +01:00
json . NewEncoder ( rw ) . Encode ( StartJobApiResponse {
2024-11-26 07:02:53 +01:00
Message : fmt . Sprintf ( "Successfully triggered job start" ) ,
2021-12-16 09:35:03 +01:00
} )
}
2022-09-15 12:37:44 +02:00
// stopJobByRequest godoc
2022-11-11 15:26:27 +01:00
// @summary Marks job as completed and triggers archiving
2023-09-07 15:14:09 +02:00
// @tags Job add and modify
2022-11-11 15:26:27 +01:00
// @description Job to stop is specified by request body. All fields are required in this case.
// @description Returns full job resource information according to 'JobMeta' scheme.
// @produce json
// @param request body api.StopJobApiRequest true "All fields required"
// @success 200 {object} schema.JobMeta "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/stop_job/ [post]
2022-09-15 12:37:44 +02:00
func ( api * RestApi ) stopJobByRequest ( rw http . ResponseWriter , r * http . Request ) {
// Parse request body
req := StopJobApiRequest { }
if err := decode ( r . Body , & req ) ; err != nil {
handleError ( fmt . Errorf ( "parsing request body failed: %w" , err ) , http . StatusBadRequest , rw )
return
}
// Fetch job (that will be stopped) from db
var job * schema . Job
var err error
if req . JobId == nil {
handleError ( errors . New ( "the field 'jobId' is required" ) , http . StatusBadRequest , rw )
return
2021-12-17 15:49:22 +01:00
}
2022-09-15 12:37:44 +02:00
2024-10-31 15:47:45 +01:00
// log.Printf("loading db job for stopJobByRequest... : stopJobApiRequest=%v", req)
2022-09-15 12:37:44 +02:00
job , err = api . JobRepository . Find ( req . JobId , req . Cluster , req . StartTime )
2021-12-17 15:49:22 +01:00
if err != nil {
2022-02-14 17:31:51 +01:00
handleError ( fmt . Errorf ( "finding job failed: %w" , err ) , http . StatusUnprocessableEntity , rw )
2021-12-16 09:35:03 +01:00
return
}
2022-09-23 11:59:18 +02:00
api . checkAndHandleStopJob ( rw , job , req )
}
2022-11-11 15:26:27 +01:00
// deleteJobById godoc
// @summary Remove a job from the sql database
2023-09-07 15:14:09 +02:00
// @tags Job remove
2022-11-11 15:26:27 +01:00
// @description Job to remove is specified by database ID. This will not remove the job from the job archive.
// @produce json
// @param id path int true "Database ID of Job"
// @success 200 {object} api.DeleteJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/delete_job/{id} [delete]
func ( api * RestApi ) deleteJobById ( rw http . ResponseWriter , r * http . Request ) {
// Fetch job (that will be stopped) from db
id , ok := mux . Vars ( r ) [ "id" ]
var err error
if ok {
id , e := strconv . ParseInt ( id , 10 , 64 )
if e != nil {
handleError ( fmt . Errorf ( "integer expected in path for id: %w" , e ) , http . StatusBadRequest , rw )
return
}
err = api . JobRepository . DeleteJobById ( id )
} else {
handleError ( errors . New ( "the parameter 'id' is required" ) , http . StatusBadRequest , rw )
return
}
if err != nil {
handleError ( fmt . Errorf ( "deleting job failed: %w" , err ) , http . StatusUnprocessableEntity , rw )
return
}
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusOK )
json . NewEncoder ( rw ) . Encode ( DeleteJobApiResponse {
Message : fmt . Sprintf ( "Successfully deleted job %s" , id ) ,
} )
}
// deleteJobByRequest godoc
// @summary Remove a job from the sql database
2023-09-07 15:14:09 +02:00
// @tags Job remove
2022-11-11 15:26:27 +01:00
// @description Job to delete is specified by request body. All fields are required in this case.
// @accept json
// @produce json
// @param request body api.DeleteJobApiRequest true "All fields required"
// @success 200 {object} api.DeleteJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/delete_job/ [delete]
func ( api * RestApi ) deleteJobByRequest ( rw http . ResponseWriter , r * http . Request ) {
// Parse request body
req := DeleteJobApiRequest { }
if err := decode ( r . Body , & req ) ; err != nil {
handleError ( fmt . Errorf ( "parsing request body failed: %w" , err ) , http . StatusBadRequest , rw )
return
}
// Fetch job (that will be deleted) from db
var job * schema . Job
var err error
if req . JobId == nil {
handleError ( errors . New ( "the field 'jobId' is required" ) , http . StatusBadRequest , rw )
return
}
job , err = api . JobRepository . Find ( req . JobId , req . Cluster , req . StartTime )
if err != nil {
handleError ( fmt . Errorf ( "finding job failed: %w" , err ) , http . StatusUnprocessableEntity , rw )
return
}
err = api . JobRepository . DeleteJobById ( job . ID )
if err != nil {
handleError ( fmt . Errorf ( "deleting job failed: %w" , err ) , http . StatusUnprocessableEntity , rw )
return
}
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusOK )
json . NewEncoder ( rw ) . Encode ( DeleteJobApiResponse {
Message : fmt . Sprintf ( "Successfully deleted job %d" , job . ID ) ,
} )
}
// deleteJobBefore godoc
// @summary Remove a job from the sql database
2023-09-07 15:14:09 +02:00
// @tags Job remove
2022-11-25 15:15:05 +01:00
// @description Remove all jobs with start time before timestamp. The jobs will not be removed from the job archive.
2022-11-11 15:26:27 +01:00
// @produce json
// @param ts path int true "Unix epoch timestamp"
// @success 200 {object} api.DeleteJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/delete_job_before/{ts} [delete]
func ( api * RestApi ) deleteJobBefore ( rw http . ResponseWriter , r * http . Request ) {
var cnt int
// Fetch job (that will be stopped) from db
id , ok := mux . Vars ( r ) [ "ts" ]
var err error
if ok {
ts , e := strconv . ParseInt ( id , 10 , 64 )
if e != nil {
handleError ( fmt . Errorf ( "integer expected in path for ts: %w" , e ) , http . StatusBadRequest , rw )
return
}
cnt , err = api . JobRepository . DeleteJobsBefore ( ts )
} else {
handleError ( errors . New ( "the parameter 'ts' is required" ) , http . StatusBadRequest , rw )
return
}
if err != nil {
handleError ( fmt . Errorf ( "deleting jobs failed: %w" , err ) , http . StatusUnprocessableEntity , rw )
return
}
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusOK )
json . NewEncoder ( rw ) . Encode ( DeleteJobApiResponse {
Message : fmt . Sprintf ( "Successfully deleted %d jobs" , cnt ) ,
} )
}
2022-09-23 11:59:18 +02:00
func ( api * RestApi ) checkAndHandleStopJob ( rw http . ResponseWriter , job * schema . Job , req StopJobApiRequest ) {
2022-02-15 13:18:27 +01:00
// Sanity checks
2021-12-17 15:49:22 +01:00
if job == nil || job . StartTime . Unix ( ) >= req . StopTime || job . State != schema . JobStateRunning {
2024-10-25 15:23:49 +02:00
handleError ( fmt . Errorf ( "jobId %d (id %d) on %s : stopTime %d must be larger than startTime %d and only running jobs can be stopped (state is: %s)" , job . JobID , job . ID , job . Cluster , req . StopTime , job . StartTime . Unix ( ) , job . State ) , http . StatusBadRequest , rw )
2021-12-16 09:35:03 +01:00
return
}
2022-09-23 11:59:18 +02:00
2021-12-17 15:49:22 +01:00
if req . State != "" && ! req . State . Valid ( ) {
2024-10-25 15:23:49 +02:00
handleError ( fmt . Errorf ( "jobId %d (id %d) on %s : invalid requested job state: %#v" , job . JobID , job . ID , job . Cluster , req . State ) , http . StatusBadRequest , rw )
2021-12-17 15:49:22 +01:00
return
2022-11-24 14:28:57 +01:00
} else if req . State == "" {
2021-12-17 15:49:22 +01:00
req . State = schema . JobStateCompleted
}
2022-02-15 13:18:27 +01:00
// Mark job as stopped in the database (update state and duration)
job . Duration = int32 ( req . StopTime - job . StartTime . Unix ( ) )
2022-02-15 14:25:39 +01:00
job . State = req . State
if err := api . JobRepository . Stop ( job . ID , job . Duration , job . State , job . MonitoringStatus ) ; err != nil {
2024-10-25 15:23:49 +02:00
handleError ( fmt . Errorf ( "jobId %d (id %d) on %s : marking job as '%s' (duration: %d) in DB failed: %w" , job . JobID , job . ID , job . Cluster , job . State , job . Duration , err ) , http . StatusInternalServerError , rw )
2022-02-15 13:18:27 +01:00
return
2021-12-16 09:35:03 +01:00
}
2024-10-25 15:23:49 +02:00
log . Printf ( "archiving job... (dbid: %d): cluster=%s, jobId=%d, user=%s, startTime=%s, duration=%d, state=%s" , job . ID , job . Cluster , job . JobID , job . User , job . StartTime , job . Duration , job . State )
2022-02-15 11:10:49 +01:00
2022-02-15 13:18:27 +01:00
// Send a response (with status OK). This means that erros that happen from here on forward
// can *NOT* be communicated to the client. If reading from a MetricDataRepository or
// writing to the filesystem fails, the client will not know.
2022-02-15 11:10:49 +01:00
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusOK )
json . NewEncoder ( rw ) . Encode ( job )
2022-02-15 13:18:27 +01:00
2022-02-15 14:25:39 +01:00
// Monitoring is disabled...
if job . MonitoringStatus == schema . MonitoringStatusDisabled {
return
}
2022-12-08 15:04:58 +01:00
// Trigger async archiving
2024-08-28 11:13:54 +02:00
archiver . TriggerArchiving ( job )
2021-12-16 09:35:03 +01:00
}
2022-01-07 09:39:00 +01:00
2022-01-27 09:29:53 +01:00
func ( api * RestApi ) getJobMetrics ( rw http . ResponseWriter , r * http . Request ) {
id := mux . Vars ( r ) [ "id" ]
metrics := r . URL . Query ( ) [ "metric" ]
var scopes [ ] schema . MetricScope
for _ , scope := range r . URL . Query ( ) [ "scope" ] {
var s schema . MetricScope
if err := s . UnmarshalGQL ( scope ) ; err != nil {
http . Error ( rw , err . Error ( ) , http . StatusBadRequest )
return
}
scopes = append ( scopes , s )
}
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusOK )
type Respone struct {
Data * struct {
JobMetrics [ ] * model . JobMetricWithName ` json:"jobMetrics" `
} ` json:"data" `
Error * struct {
Message string ` json:"message" `
} ` json:"error" `
}
2024-07-16 12:08:10 +02:00
resolver := graph . GetResolverInstance ( )
2024-08-16 14:50:31 +02:00
data , err := resolver . Query ( ) . JobMetrics ( r . Context ( ) , id , metrics , scopes , nil )
2022-01-27 09:29:53 +01:00
if err != nil {
2022-01-27 10:35:26 +01:00
json . NewEncoder ( rw ) . Encode ( Respone {
2022-01-27 09:29:53 +01:00
Error : & struct {
Message string "json:\"message\""
} { Message : err . Error ( ) } ,
2022-01-27 10:35:26 +01:00
} )
2022-01-27 09:29:53 +01:00
return
}
2022-01-27 10:35:26 +01:00
json . NewEncoder ( rw ) . Encode ( Respone {
2022-01-27 09:29:53 +01:00
Data : & struct {
JobMetrics [ ] * model . JobMetricWithName "json:\"jobMetrics\""
} { JobMetrics : data } ,
2022-01-27 10:35:26 +01:00
} )
2022-01-27 09:29:53 +01:00
}
2023-08-18 17:18:31 +02:00
// createUser godoc
// @summary Adds a new user
2023-09-07 15:14:09 +02:00
// @tags User
2023-08-18 17:18:31 +02:00
// @description User specified in form data will be saved to database.
2023-09-07 14:03:41 +02:00
// @description Only accessible from IPs registered with apiAllowedIPs configuration option.
2023-08-18 17:18:31 +02:00
// @accept mpfd
// @produce plain
// @param username formData string true "Unique user ID"
// @param password formData string true "User password"
2023-08-21 12:12:28 +02:00
// @param role formData string true "User role" Enums(admin, support, manager, user, api)
2023-08-18 17:18:31 +02:00
// @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"
2023-08-21 12:12:28 +02:00
// @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"
2023-08-18 17:18:31 +02:00
// @security ApiKeyAuth
// @router /users/ [post]
2022-03-02 10:50:08 +01:00
func ( api * RestApi ) createUser ( rw http . ResponseWriter , r * http . Request ) {
2023-08-14 18:38:30 +02:00
err := securedCheck ( r )
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusForbidden )
2023-08-18 17:18:31 +02:00
return
2023-08-14 18:38:30 +02:00
}
2022-03-03 14:54:37 +01:00
rw . Header ( ) . Set ( "Content-Type" , "text/plain" )
2023-08-17 10:29:00 +02:00
me := repository . GetUserFromContext ( r . Context ( ) )
if ! me . HasRole ( schema . RoleAdmin ) {
2023-02-01 11:58:27 +01:00
http . Error ( rw , "Only admins are allowed to create new users" , http . StatusForbidden )
2022-03-02 10:50:08 +01:00
return
}
2023-08-14 18:38:30 +02:00
username , password , role , name , email , project := r . FormValue ( "username" ) ,
r . FormValue ( "password" ) , r . FormValue ( "role" ) , r . FormValue ( "name" ) ,
r . FormValue ( "email" ) , r . FormValue ( "project" )
2023-08-17 10:29:00 +02:00
if len ( password ) == 0 && role != schema . GetRoleString ( schema . RoleApi ) {
2023-02-01 11:58:27 +01:00
http . Error ( rw , "Only API users are allowed to have a blank password (login will be impossible)" , http . StatusBadRequest )
2022-03-02 10:50:08 +01:00
return
}
2023-08-17 10:29:00 +02:00
if len ( project ) != 0 && role != schema . GetRoleString ( schema . RoleManager ) {
2023-08-14 18:38:30 +02:00
http . Error ( rw , "only managers require a project (can be changed later)" ,
http . StatusBadRequest )
2023-01-27 18:36:58 +01:00
return
2023-08-17 10:29:00 +02:00
} else if len ( project ) == 0 && role == schema . GetRoleString ( schema . RoleManager ) {
2023-08-14 18:38:30 +02:00
http . Error ( rw , "managers require a project to manage (can be changed later)" ,
http . StatusBadRequest )
2023-01-27 18:36:58 +01:00
return
}
2023-08-17 10:29:00 +02:00
if err := repository . GetUserRepository ( ) . AddUser ( & schema . User {
2022-07-07 14:08:37 +02:00
Username : username ,
Name : name ,
Password : password ,
Email : email ,
2023-02-17 15:45:31 +01:00
Projects : [ ] string { project } ,
2024-03-08 08:51:05 +01:00
Roles : [ ] string { role } ,
} ) ; err != nil {
2022-03-03 14:54:37 +01:00
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
2024-03-19 16:18:43 +01:00
fmt . Fprintf ( rw , "User %v successfully created!\n" , username )
2022-03-03 14:54:37 +01:00
}
2023-08-18 17:18:31 +02:00
// deleteUser godoc
// @summary Deletes a user
2023-09-07 15:14:09 +02:00
// @tags User
2023-08-18 17:18:31 +02:00
// @description User defined by username in form data will be deleted from database.
2023-09-07 14:03:41 +02:00
// @description Only accessible from IPs registered with apiAllowedIPs configuration option.
2023-08-18 17:18:31 +02:00
// @accept mpfd
2023-08-21 12:12:28 +02:00
// @produce plain
// @param username formData string true "User ID to delete"
2023-08-18 17:18:31 +02:00
// @success 200 "User deleted successfully"
2023-08-21 12:12:28 +02:00
// @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"
2023-08-18 17:18:31 +02:00
// @security ApiKeyAuth
// @router /users/ [delete]
2022-03-03 14:54:37 +01:00
func ( api * RestApi ) deleteUser ( rw http . ResponseWriter , r * http . Request ) {
2023-08-14 18:38:30 +02:00
err := securedCheck ( r )
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusForbidden )
2023-08-18 17:18:31 +02:00
return
2023-08-14 18:38:30 +02:00
}
2023-08-17 10:29:00 +02:00
if user := repository . GetUserFromContext ( r . Context ( ) ) ; ! user . HasRole ( schema . RoleAdmin ) {
2023-02-01 11:58:27 +01:00
http . Error ( rw , "Only admins are allowed to delete a user" , http . StatusForbidden )
2022-03-03 14:54:37 +01:00
return
}
username := r . FormValue ( "username" )
2023-08-17 10:29:00 +02:00
if err := repository . GetUserRepository ( ) . DelUser ( username ) ; err != nil {
2022-03-02 10:50:08 +01:00
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
rw . WriteHeader ( http . StatusOK )
}
2023-08-18 17:18:31 +02:00
// getUsers godoc
// @summary Returns a list of users
2023-09-07 15:14:09 +02:00
// @tags User
2023-08-18 17:18:31 +02:00
// @description Returns a JSON-encoded list of users.
// @description Required query-parameter defines if all users or only users with additional special roles are returned.
2023-09-07 14:03:41 +02:00
// @description Only accessible from IPs registered with apiAllowedIPs configuration option.
2023-08-18 17:18:31 +02:00
// @produce json
// @param not-just-user query bool true "If returned list should contain all users or only users with additional special roles"
2023-08-21 12:12:28 +02:00
// @success 200 {array} api.ApiReturnedUser "List of users returned successfully"
// @failure 400 {string} string "Bad Request"
// @failure 401 {string} string "Unauthorized"
// @failure 403 {string} string "Forbidden"
// @failure 500 {string} string "Internal Server Error"
2023-08-18 17:18:31 +02:00
// @security ApiKeyAuth
// @router /users/ [get]
2022-03-03 14:54:37 +01:00
func ( api * RestApi ) getUsers ( rw http . ResponseWriter , r * http . Request ) {
2023-08-14 18:38:30 +02:00
err := securedCheck ( r )
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusForbidden )
2023-08-18 17:18:31 +02:00
return
2023-08-14 18:38:30 +02:00
}
2023-08-17 10:29:00 +02:00
if user := repository . GetUserFromContext ( r . Context ( ) ) ; ! user . HasRole ( schema . RoleAdmin ) {
2023-02-01 11:58:27 +01:00
http . Error ( rw , "Only admins are allowed to fetch a list of users" , http . StatusForbidden )
2022-03-03 14:54:37 +01:00
return
}
2023-08-17 10:29:00 +02:00
users , err := repository . GetUserRepository ( ) . ListUsers ( r . URL . Query ( ) . Get ( "not-just-user" ) == "true" )
2022-03-03 14:54:37 +01:00
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusInternalServerError )
return
}
json . NewEncoder ( rw ) . Encode ( users )
}
2023-08-18 17:18:31 +02:00
// updateUser godoc
// @summary Updates an existing user
2023-09-07 15:14:09 +02:00
// @tags User
2023-08-18 17:18:31 +02:00
// @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.
2023-09-07 14:03:41 +02:00
// @description Only accessible from IPs registered with apiAllowedIPs configuration option.
2023-08-18 17:18:31 +02:00
// @accept mpfd
// @produce plain
2023-08-21 12:12:28 +02:00
// @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"
2023-08-18 17:18:31 +02:00
// @security ApiKeyAuth
// @router /user/{id} [post]
2022-04-11 12:29:24 +02:00
func ( api * RestApi ) updateUser ( rw http . ResponseWriter , r * http . Request ) {
2023-08-14 18:38:30 +02:00
err := securedCheck ( r )
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusForbidden )
2023-08-18 17:18:31 +02:00
return
2023-08-14 18:38:30 +02:00
}
2023-08-17 10:29:00 +02:00
if user := repository . GetUserFromContext ( r . Context ( ) ) ; ! user . HasRole ( schema . RoleAdmin ) {
2023-02-01 11:58:27 +01:00
http . Error ( rw , "Only admins are allowed to update a user" , http . StatusForbidden )
2022-04-11 12:29:24 +02:00
return
}
2022-08-26 15:15:36 +02:00
// Get Values
2022-04-11 12:29:24 +02:00
newrole := r . FormValue ( "add-role" )
2022-08-26 15:15:36 +02:00
delrole := r . FormValue ( "remove-role" )
2023-01-27 18:36:58 +01:00
newproj := r . FormValue ( "add-project" )
delproj := r . FormValue ( "remove-project" )
2022-04-11 12:29:24 +02:00
2022-08-26 15:15:36 +02:00
// TODO: Handle anything but roles...
2022-11-09 11:49:36 +01:00
if newrole != "" {
2023-08-17 10:29:00 +02:00
if err := repository . GetUserRepository ( ) . AddRole ( r . Context ( ) , mux . Vars ( r ) [ "id" ] , newrole ) ; err != nil {
2022-08-26 15:15:36 +02:00
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
rw . Write ( [ ] byte ( "Add Role Success" ) )
2022-11-09 11:49:36 +01:00
} else if delrole != "" {
2023-08-17 10:29:00 +02:00
if err := repository . GetUserRepository ( ) . RemoveRole ( r . Context ( ) , mux . Vars ( r ) [ "id" ] , delrole ) ; err != nil {
2022-08-26 15:15:36 +02:00
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
rw . Write ( [ ] byte ( "Remove Role Success" ) )
2023-01-27 18:36:58 +01:00
} else if newproj != "" {
2023-08-17 10:29:00 +02:00
if err := repository . GetUserRepository ( ) . AddProject ( r . Context ( ) , mux . Vars ( r ) [ "id" ] , newproj ) ; err != nil {
2023-02-17 15:45:31 +01:00
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
rw . Write ( [ ] byte ( "Add Project Success" ) )
2023-01-27 18:36:58 +01:00
} else if delproj != "" {
2023-08-17 10:29:00 +02:00
if err := repository . GetUserRepository ( ) . RemoveProject ( r . Context ( ) , mux . Vars ( r ) [ "id" ] , delproj ) ; err != nil {
2023-02-17 15:45:31 +01:00
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
rw . Write ( [ ] byte ( "Remove Project Success" ) )
2022-08-26 15:15:36 +02:00
} else {
2023-01-27 18:36:58 +01:00
http . Error ( rw , "Not Add or Del [role|project]?" , http . StatusInternalServerError )
2022-08-26 15:15:36 +02:00
}
2022-04-11 12:29:24 +02:00
}
2023-08-18 17:18:31 +02:00
func ( api * RestApi ) getJWT ( rw http . ResponseWriter , r * http . Request ) {
err := securedCheck ( r )
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusForbidden )
return
}
rw . Header ( ) . Set ( "Content-Type" , "text/plain" )
username := r . FormValue ( "username" )
me := repository . GetUserFromContext ( r . Context ( ) )
if ! me . HasRole ( schema . RoleAdmin ) {
if username != me . Username {
http . Error ( rw , "Only admins are allowed to sign JWTs not for themselves" ,
http . StatusForbidden )
return
}
}
user , err := repository . GetUserRepository ( ) . GetUser ( username )
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
jwt , err := api . Authentication . JwtAuth . ProvideJWT ( user )
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
rw . WriteHeader ( http . StatusOK )
rw . Write ( [ ] byte ( jwt ) )
}
func ( api * RestApi ) getRoles ( rw http . ResponseWriter , r * http . Request ) {
err := securedCheck ( r )
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusForbidden )
return
}
user := repository . GetUserFromContext ( r . Context ( ) )
if ! user . HasRole ( schema . RoleAdmin ) {
http . Error ( rw , "only admins are allowed to fetch a list of roles" , http . StatusForbidden )
return
}
roles , err := schema . GetValidRoles ( user )
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusInternalServerError )
return
}
json . NewEncoder ( rw ) . Encode ( roles )
}
2022-03-02 10:50:08 +01:00
func ( api * RestApi ) updateConfiguration ( rw http . ResponseWriter , r * http . Request ) {
2022-03-03 14:54:37 +01:00
rw . Header ( ) . Set ( "Content-Type" , "text/plain" )
2022-03-02 10:50:08 +01:00
key , value := r . FormValue ( "key" ) , r . FormValue ( "value" )
2022-03-03 14:54:37 +01:00
2023-01-23 18:48:06 +01:00
fmt . Printf ( "REST > KEY: %#v\nVALUE: %#v\n" , key , value )
2022-03-03 14:54:37 +01:00
2023-08-17 10:29:00 +02:00
if err := repository . GetUserCfgRepo ( ) . UpdateConfig ( key , value , repository . GetUserFromContext ( r . Context ( ) ) ) ; err != nil {
2022-03-02 10:50:08 +01:00
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
2022-03-03 14:54:37 +01:00
rw . Write ( [ ] byte ( "success" ) )
2022-03-02 10:50:08 +01:00
}
2022-01-07 09:39:00 +01:00
func ( api * RestApi ) putMachineState ( rw http . ResponseWriter , r * http . Request ) {
if api . MachineStateDir == "" {
2023-01-23 18:48:06 +01:00
http . Error ( rw , "REST > machine state not enabled" , http . StatusNotFound )
2022-01-07 09:39:00 +01:00
return
}
vars := mux . Vars ( r )
cluster := vars [ "cluster" ]
host := vars [ "host" ]
dir := filepath . Join ( api . MachineStateDir , cluster )
if err := os . MkdirAll ( dir , 0755 ) ; err != nil {
http . Error ( rw , err . Error ( ) , http . StatusInternalServerError )
return
}
filename := filepath . Join ( dir , fmt . Sprintf ( "%s.json" , host ) )
f , err := os . Create ( filename )
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusInternalServerError )
return
}
defer f . Close ( )
if _ , err := io . Copy ( f , r . Body ) ; err != nil {
http . Error ( rw , err . Error ( ) , http . StatusInternalServerError )
return
}
rw . WriteHeader ( http . StatusCreated )
}
func ( api * RestApi ) getMachineState ( rw http . ResponseWriter , r * http . Request ) {
if api . MachineStateDir == "" {
2023-01-23 18:48:06 +01:00
http . Error ( rw , "REST > machine state not enabled" , http . StatusNotFound )
2022-01-07 09:39:00 +01:00
return
}
vars := mux . Vars ( r )
filename := filepath . Join ( api . MachineStateDir , vars [ "cluster" ] , fmt . Sprintf ( "%s.json" , vars [ "host" ] ) )
// Sets the content-type and 'Last-Modified' Header and so on automatically
http . ServeFile ( rw , r , filename )
}