2023-08-14 18:38:30 +02:00
// Copyright (C) 2023 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
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"
2023-06-14 14:33:36 +02:00
"github.com/ClusterCockpit/cc-backend/internal/metricdata"
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
// @tag.name Job API
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
Resolver * graph . Resolver
Authentication * auth . Authentication
MachineStateDir string
RepositoryMutex sync . Mutex
2021-12-16 09:35:03 +01:00
}
func ( api * RestApi ) MountRoutes ( r * mux . Router ) {
2022-01-10 16:14:54 +01:00
r = r . PathPrefix ( "/api" ) . Subrouter ( )
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 )
r . HandleFunc ( "/jobs/stop_job/{id}" , api . stopJobById ) . 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 )
2022-01-10 16:14:54 +01:00
r . HandleFunc ( "/jobs/tag_job/{id}" , api . tagJob ) . 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 )
2023-08-14 14:33:05 +02:00
// r.HandleFunc("/secured/addProject/{id}/{project}", api.secureUpdateUser).Methods(http.MethodPost)
// r.HandleFunc("/secured/addRole/{id}/{role}", api.secureUpdateUser).Methods(http.MethodPost)
2022-03-02 10:50:08 +01:00
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 )
}
2023-08-14 14:33:05 +02:00
if api . Authentication != nil {
2023-08-14 18:38:30 +02:00
r . HandleFunc ( "/jwt/" , api . getJWT ) . Methods ( http . MethodGet )
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 )
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 {
2022-09-15 12:37:44 +02:00
// Database ID of new job
2021-12-16 09:35:03 +01:00
DBID int64 ` json:"id" `
}
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 {
2022-11-09 11:49:36 +01:00
// Stop Time of job as epoch
2022-09-21 11:54:19 +02:00
StopTime int64 ` json:"stopTime" validate:"required" example:"1649763839" `
2023-03-01 10:49:08 +01:00
State schema . JobState ` json:"jobState" validate:"required" example:"completed" ` // Final job state
JobId * int64 ` json:"jobId" 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
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
}
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
2022-09-21 11:54:19 +02:00
Type string ` json:"type" example:"Debug" `
Name string ` json:"name" example:"Testjob" ` // Tag Name
2022-02-14 17:31:51 +01:00
}
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
}
type JobMetricWithName struct {
Name string ` json:"name" `
Scope schema . MetricScope ` json:"scope" `
Metric * schema . JobMetric ` json:"metric" `
2023-06-14 14:33:36 +02:00
}
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 {
user := auth . GetUser ( r . Context ( ) )
if user == nil {
return fmt . Errorf ( "no user in context" )
}
if user . AuthType == auth . AuthToken {
// 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" )
}
// extract IP address
IPAddress := r . Header . Get ( "X-Real-Ip" )
if IPAddress == "" {
IPAddress = r . Header . Get ( "X-Forwarded-For" )
}
if IPAddress == "" {
IPAddress = r . RemoteAddr
}
// check if IP is allowed
if ! util . Contains ( config . Keys . ApiAllowedIPs , IPAddress ) {
return fmt . Errorf ( "unknown ip: %v" , IPAddress )
}
}
return nil
}
2022-09-15 12:37:44 +02:00
// getJobs godoc
2022-11-11 15:26:27 +01:00
// @summary Lists all jobs
// @tags query
// @description Get a list of all jobs. Filters can be applied using query parameters.
// @description Number of results can be limited by page. Results are sorted by descending startTime.
// @produce json
// @param state query string false "Job State" Enums(running, completed, failed, cancelled, stopped, timeout)
// @param cluster query string false "Job Cluster"
// @param start-time query string false "Syntax: '$from-$to', as unix epoch timestamps in seconds"
// @param items-per-page query int false "Items per page (Default: 25)"
// @param page query int false "Page Number (Default: 1)"
// @param with-metadata query bool false "Include metadata (e.g. jobScript) in response"
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 ) {
2023-06-20 12:54:26 +02:00
2022-11-11 15:26:27 +01:00
if user := auth . GetUser ( r . Context ( ) ) ; user != nil && ! user . HasRole ( auth . RoleApi ) {
2023-03-06 11:44:38 +01:00
handleError ( fmt . Errorf ( "missing role: %v" , auth . GetRoleString ( auth . RoleApi ) ) , http . StatusForbidden , rw )
2022-11-11 15:26:27 +01:00
return
}
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
2022-02-22 10:51:58 +01:00
res . Tags , err = api . JobRepository . GetTags ( & job . ID )
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 {
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
}
}
}
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
}
}
// getJobById godoc
// @summary Get complete job meta and metric data
// @tags 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'.
// @accept json
// @produce json
// @param id path int true "Database ID of Job"
2023-06-14 15:03:01 +02:00
// @param request body api.GetJobApiRequest true "Array of metric names"
// @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 ) {
if user := auth . GetUser ( r . Context ( ) ) ; user != nil && ! user . HasRole ( auth . RoleApi ) {
handleError ( fmt . Errorf ( "missing role: %v" ,
auth . GetRoleString ( auth . RoleApi ) ) , http . StatusForbidden , rw )
return
}
// 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
}
job , err = api . JobRepository . FindById ( id )
} else {
handleError ( errors . New ( "the parameter 'id' is required" ) , http . StatusBadRequest , rw )
return
}
if err != nil {
handleError ( fmt . Errorf ( "finding job failed: %w" , err ) , http . StatusUnprocessableEntity , rw )
return
}
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" }
}
data , err := metricdata . LoadData ( job , metrics , scopes , r . Context ( ) )
if err != nil {
log . Warn ( "Error while loading job data" )
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
}
}
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
// @tags add and modify
// @description Adds tag(s) to a job specified by DB ID. Name and Type of Tag(s) can be chosen freely.
// @description If tagged job is already finished: Tag will be written directly to respective archive files.
// @accept json
// @produce json
// @param id path int true "Job Database ID"
// @param request body api.TagJobApiRequest true "Array of tag-objects to add"
// @success 200 {object} schema.Job "Updated job resource"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 404 {object} api.ErrorResponse "Job or tag does not exist"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/tag_job/{id} [post]
2021-12-16 09:35:03 +01:00
func ( api * RestApi ) tagJob ( rw http . ResponseWriter , r * http . Request ) {
2022-11-11 15:26:27 +01:00
if user := auth . GetUser ( r . Context ( ) ) ; user != nil && ! user . HasRole ( auth . RoleApi ) {
2023-03-06 11:44:38 +01:00
handleError ( fmt . Errorf ( "missing role: %v" , auth . GetRoleString ( auth . RoleApi ) ) , http . StatusForbidden , rw )
2022-11-11 15:26:27 +01:00
return
}
2022-02-22 10:51:58 +01:00
iid , 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 ( iid )
2021-12-16 09:35:03 +01:00
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusNotFound )
return
}
2022-02-22 10:51:58 +01:00
job . Tags , err = api . JobRepository . GetTags ( & 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 {
2022-02-08 12:49:28 +01:00
tagId , err := api . JobRepository . AddTagOrCreate ( job . ID , tag . Type , tag . Name )
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 {
ID : tagId ,
Type : tag . Type ,
Name : tag . Name ,
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"
// @tags add and modify
// @description Job specified in request body will be saved to database as "running" with new DB ID.
// @description Job specifications follow the 'JobMeta' scheme, API will fail to execute if requirements are not met.
// @accept json
// @produce json
// @param request body schema.JobMeta true "Job to add"
// @success 201 {object} api.StartJobApiResponse "Job added successfully"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: The combination of jobId, clusterId and startTime does already exist"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/start_job/ [post]
2021-12-16 09:35:03 +01:00
func ( api * RestApi ) startJob ( rw http . ResponseWriter , r * http . Request ) {
2022-01-27 09:29:11 +01:00
if user := auth . GetUser ( r . Context ( ) ) ; user != nil && ! user . HasRole ( auth . RoleApi ) {
2023-03-06 11:44:38 +01:00
handleError ( fmt . Errorf ( "missing role: %v" , auth . GetRoleString ( auth . RoleApi ) ) , http . StatusForbidden , rw )
2022-01-27 09:29:11 +01:00
return
}
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
}
2022-11-24 14:28:19 +01:00
// aquire lock to avoid race condition between API calls
var unlockOnce sync . Once
api . RepositoryMutex . Lock ( )
defer unlockOnce . Do ( api . RepositoryMutex . Unlock )
2021-12-16 09:35:03 +01:00
// 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
}
2022-02-08 12:49:28 +01:00
id , err := api . JobRepository . Start ( & req )
2021-12-16 09:35:03 +01:00
if err != nil {
2022-02-14 17:40:47 +01:00
handleError ( fmt . Errorf ( "insert into database failed: %w" , err ) , http . StatusInternalServerError , rw )
2021-12-16 09:35:03 +01:00
return
}
2022-11-24 14:28:19 +01:00
// unlock here, adding Tags can be async
unlockOnce . Do ( api . RepositoryMutex . Unlock )
2021-12-16 09:35:03 +01:00
2022-02-08 12:49:28 +01:00
for _ , tag := range req . Tags {
if _ , err := api . JobRepository . AddTagOrCreate ( id , tag . Type , tag . Name ) ; err != nil {
http . Error ( rw , err . Error ( ) , http . StatusInternalServerError )
2022-02-14 17:40:47 +01:00
handleError ( fmt . Errorf ( "adding tag to new job %d failed: %w" , id , err ) , http . StatusInternalServerError , rw )
2022-02-08 12:49:28 +01:00
return
}
2021-12-16 09:35:03 +01:00
}
2022-01-27 10:35:26 +01:00
log . Printf ( "new job (id: %d): cluster=%s, jobId=%d, user=%s, startTime=%d" , id , req . Cluster , req . JobID , req . User , req . StartTime )
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 {
2021-12-16 09:35:03 +01:00
DBID : id ,
} )
}
2022-09-15 12:37:44 +02:00
// stopJobById godoc
2022-11-11 15:26:27 +01:00
// @summary Marks job as completed and triggers archiving
// @tags add and modify
// @description Job to stop is specified by database ID. Only stopTime and final state are required in request body.
// @description Returns full job resource information according to 'JobMeta' scheme.
// @accept json
// @produce json
// @param id path int true "Database ID of Job"
// @param request body api.StopJobApiRequest true "stopTime and final state in request body"
// @success 200 {object} schema.JobMeta "Job resource"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/stop_job/{id} [post]
2022-09-15 12:37:44 +02:00
func ( api * RestApi ) stopJobById ( rw http . ResponseWriter , r * http . Request ) {
2022-01-27 09:29:11 +01:00
if user := auth . GetUser ( r . Context ( ) ) ; user != nil && ! user . HasRole ( auth . RoleApi ) {
2023-03-06 11:44:38 +01:00
handleError ( fmt . Errorf ( "missing role: %v" , auth . GetRoleString ( auth . RoleApi ) ) , http . StatusForbidden , rw )
2022-01-27 09:29:11 +01:00
return
}
2022-09-15 12:37:44 +02:00
// Parse request body: Only StopTime and State
2021-12-16 09:35:03 +01:00
req := StopJobApiRequest { }
2022-02-22 10:51:58 +01:00
if err := decode ( r . Body , & req ) ; err != nil {
2022-02-14 17:31:51 +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-15 13:18:27 +01:00
// Fetch job (that will be stopped) from db
2021-12-16 09:35:03 +01:00
id , ok := mux . Vars ( r ) [ "id" ]
2022-02-07 07:09:47 +01:00
var job * schema . Job
var err error
2021-12-16 09:35:03 +01:00
if ok {
2022-02-08 11:10:05 +01:00
id , e := strconv . ParseInt ( id , 10 , 64 )
if e != nil {
2022-02-14 17:31:51 +01:00
handleError ( fmt . Errorf ( "integer expected in path for id: %w" , e ) , http . StatusBadRequest , rw )
2022-02-08 11:10:05 +01:00
return
2022-02-07 09:57:06 +01:00
}
job , err = api . JobRepository . FindById ( id )
2021-12-16 09:35:03 +01:00
} else {
2022-09-16 06:09:55 +02:00
handleError ( errors . New ( "the parameter 'id' is required" ) , http . StatusBadRequest , rw )
return
2022-09-15 12:37:44 +02:00
}
if err != nil {
handleError ( fmt . Errorf ( "finding job failed: %w" , err ) , http . StatusUnprocessableEntity , rw )
return
}
2022-09-23 11:59:18 +02:00
api . checkAndHandleStopJob ( rw , job , req )
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
// @tags add and modify
// @description Job to stop is specified by request body. All fields are required in this case.
// @description Returns full job resource information according to 'JobMeta' scheme.
// @produce json
// @param request body api.StopJobApiRequest true "All fields required"
// @success 200 {object} schema.JobMeta "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/stop_job/ [post]
2022-09-15 12:37:44 +02:00
func ( api * RestApi ) stopJobByRequest ( rw http . ResponseWriter , r * http . Request ) {
if user := auth . GetUser ( r . Context ( ) ) ; user != nil && ! user . HasRole ( auth . RoleApi ) {
2023-03-06 11:44:38 +01:00
handleError ( fmt . Errorf ( "missing role: %v" , auth . GetRoleString ( auth . RoleApi ) ) , http . StatusForbidden , rw )
2022-09-15 12:37:44 +02:00
return
}
// Parse request body
req := StopJobApiRequest { }
if err := decode ( r . Body , & req ) ; err != nil {
handleError ( fmt . Errorf ( "parsing request body failed: %w" , err ) , http . StatusBadRequest , rw )
return
}
// Fetch job (that will be stopped) from db
var job * schema . Job
var err error
if req . JobId == nil {
handleError ( errors . New ( "the field 'jobId' is required" ) , http . StatusBadRequest , rw )
return
2021-12-17 15:49:22 +01:00
}
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
// @tags remove
// @description Job to remove is specified by database ID. This will not remove the job from the job archive.
// @produce json
// @param id path int true "Database ID of Job"
// @success 200 {object} api.DeleteJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/delete_job/{id} [delete]
func ( api * RestApi ) deleteJobById ( rw http . ResponseWriter , r * http . Request ) {
if user := auth . GetUser ( r . Context ( ) ) ; user != nil && ! user . HasRole ( auth . RoleApi ) {
2023-03-06 11:44:38 +01:00
handleError ( fmt . Errorf ( "missing role: %v" , auth . GetRoleString ( auth . RoleApi ) ) , http . StatusForbidden , rw )
2022-11-11 15:26:27 +01:00
return
}
// Fetch job (that will be stopped) from db
id , ok := mux . Vars ( r ) [ "id" ]
var err error
if ok {
id , e := strconv . ParseInt ( id , 10 , 64 )
if e != nil {
handleError ( fmt . Errorf ( "integer expected in path for id: %w" , e ) , http . StatusBadRequest , rw )
return
}
err = api . JobRepository . DeleteJobById ( id )
} else {
handleError ( errors . New ( "the parameter 'id' is required" ) , http . StatusBadRequest , rw )
return
}
if err != nil {
handleError ( fmt . Errorf ( "deleting job failed: %w" , err ) , http . StatusUnprocessableEntity , rw )
return
}
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusOK )
json . NewEncoder ( rw ) . Encode ( DeleteJobApiResponse {
Message : fmt . Sprintf ( "Successfully deleted job %s" , id ) ,
} )
}
// deleteJobByRequest godoc
// @summary Remove a job from the sql database
// @tags remove
// @description Job to delete is specified by request body. All fields are required in this case.
// @accept json
// @produce json
// @param request body api.DeleteJobApiRequest true "All fields required"
// @success 200 {object} api.DeleteJobApiResponse "Success message"
// @failure 400 {object} api.ErrorResponse "Bad Request"
// @failure 401 {object} api.ErrorResponse "Unauthorized"
// @failure 403 {object} api.ErrorResponse "Forbidden"
// @failure 404 {object} api.ErrorResponse "Resource not found"
// @failure 422 {object} api.ErrorResponse "Unprocessable Entity: finding job failed: sql: no rows in result set"
// @failure 500 {object} api.ErrorResponse "Internal Server Error"
// @security ApiKeyAuth
// @router /jobs/delete_job/ [delete]
func ( api * RestApi ) deleteJobByRequest ( rw http . ResponseWriter , r * http . Request ) {
if user := auth . GetUser ( r . Context ( ) ) ; user != nil && ! user . HasRole ( auth . RoleApi ) {
2023-03-06 11:44:38 +01:00
handleError ( fmt . Errorf ( "missing role: %v" , auth . GetRoleString ( auth . RoleApi ) ) , http . StatusForbidden , rw )
2022-11-11 15:26:27 +01:00
return
}
// Parse request body
req := DeleteJobApiRequest { }
if err := decode ( r . Body , & req ) ; err != nil {
handleError ( fmt . Errorf ( "parsing request body failed: %w" , err ) , http . StatusBadRequest , rw )
return
}
// Fetch job (that will be deleted) from db
var job * schema . Job
var err error
if req . JobId == nil {
handleError ( errors . New ( "the field 'jobId' is required" ) , http . StatusBadRequest , rw )
return
}
job , err = api . JobRepository . Find ( req . JobId , req . Cluster , req . StartTime )
if err != nil {
handleError ( fmt . Errorf ( "finding job failed: %w" , err ) , http . StatusUnprocessableEntity , rw )
return
}
err = api . JobRepository . DeleteJobById ( job . ID )
if err != nil {
handleError ( fmt . Errorf ( "deleting job failed: %w" , err ) , http . StatusUnprocessableEntity , rw )
return
}
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusOK )
json . NewEncoder ( rw ) . Encode ( DeleteJobApiResponse {
Message : fmt . Sprintf ( "Successfully deleted job %d" , job . ID ) ,
} )
}
// deleteJobBefore godoc
// @summary Remove a job from the sql database
// @tags remove
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 ) {
if user := auth . GetUser ( r . Context ( ) ) ; user != nil && ! user . HasRole ( auth . RoleApi ) {
2023-03-06 11:44:38 +01:00
handleError ( fmt . Errorf ( "missing role: %v" , auth . GetRoleString ( auth . RoleApi ) ) , http . StatusForbidden , rw )
2022-11-11 15:26:27 +01:00
return
}
var cnt int
// Fetch job (that will be stopped) from db
id , ok := mux . Vars ( r ) [ "ts" ]
var err error
if ok {
ts , e := strconv . ParseInt ( id , 10 , 64 )
if e != nil {
handleError ( fmt . Errorf ( "integer expected in path for ts: %w" , e ) , http . StatusBadRequest , rw )
return
}
cnt , err = api . JobRepository . DeleteJobsBefore ( ts )
} else {
handleError ( errors . New ( "the parameter 'ts' is required" ) , http . StatusBadRequest , rw )
return
}
if err != nil {
handleError ( fmt . Errorf ( "deleting jobs failed: %w" , err ) , http . StatusUnprocessableEntity , rw )
return
}
rw . Header ( ) . Add ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusOK )
json . NewEncoder ( rw ) . Encode ( DeleteJobApiResponse {
Message : fmt . Sprintf ( "Successfully deleted %d jobs" , cnt ) ,
} )
}
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 {
2022-02-14 17:31:51 +01:00
handleError ( errors . New ( "stopTime must be larger than startTime and only running jobs can be stopped" ) , 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 ( ) {
2022-02-14 17:31:51 +01:00
handleError ( fmt . Errorf ( "invalid job state: %#v" , 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 {
2022-02-15 13:18:27 +01:00
handleError ( fmt . Errorf ( "marking job as stopped failed: %w" , err ) , http . StatusInternalServerError , rw )
return
2021-12-16 09:35:03 +01:00
}
2022-01-27 10:35:26 +01:00
log . Printf ( "archiving job... (dbid: %d): cluster=%s, jobId=%d, user=%s, startTime=%s" , job . ID , job . Cluster , job . JobID , job . User , job . StartTime )
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
api . JobRepository . 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" `
}
data , err := api . Resolver . Query ( ) . JobMetrics ( r . Context ( ) , id , metrics , scopes )
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
}
2022-03-02 10:50:08 +01:00
func ( api * RestApi ) getJWT ( 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 )
}
2022-03-03 14:54:37 +01:00
rw . Header ( ) . Set ( "Content-Type" , "text/plain" )
2022-03-02 10:50:08 +01:00
username := r . FormValue ( "username" )
me := auth . GetUser ( r . Context ( ) )
if ! me . HasRole ( auth . RoleAdmin ) {
if username != me . Username {
2023-02-25 07:21:54 +01:00
http . Error ( rw , "Only admins are allowed to sign JWTs not for themselves" ,
http . StatusForbidden )
2022-03-02 10:50:08 +01:00
return
}
}
2022-07-07 14:08:37 +02:00
user , err := api . Authentication . GetUser ( username )
2022-03-02 10:50:08 +01:00
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
2022-07-07 14:08:37 +02:00
jwt , err := api . Authentication . JwtAuth . ProvideJWT ( user )
2022-03-02 10:50:08 +01:00
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
rw . WriteHeader ( http . StatusOK )
rw . Write ( [ ] byte ( jwt ) )
}
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 )
}
2022-03-03 14:54:37 +01:00
rw . Header ( ) . Set ( "Content-Type" , "text/plain" )
2022-03-02 10:50:08 +01:00
me := auth . GetUser ( r . Context ( ) )
if ! me . HasRole ( auth . 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-03-06 11:44:38 +01:00
if len ( password ) == 0 && role != auth . GetRoleString ( auth . 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-03-06 11:44:38 +01:00
if len ( project ) != 0 && role != auth . GetRoleString ( auth . 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-03-06 11:44:38 +01:00
} else if len ( project ) == 0 && role == auth . GetRoleString ( auth . 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
}
2022-07-07 14:08:37 +02:00
if err := api . Authentication . AddUser ( & auth . User {
Username : username ,
Name : name ,
Password : password ,
Email : email ,
2023-02-17 15:45:31 +01:00
Projects : [ ] string { project } ,
2022-07-07 14:08:37 +02:00
Roles : [ ] string { role } } ) ; err != nil {
2022-03-03 14:54:37 +01:00
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
2023-02-01 11:58:27 +01:00
rw . Write ( [ ] byte ( fmt . Sprintf ( "User %v successfully created!\n" , username ) ) )
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 )
}
2022-03-03 14:54:37 +01:00
if user := auth . GetUser ( r . Context ( ) ) ; ! user . HasRole ( auth . 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" )
if err := api . Authentication . DelUser ( username ) ; err != nil {
2022-03-02 10:50:08 +01:00
http . Error ( rw , err . Error ( ) , http . StatusUnprocessableEntity )
return
}
rw . WriteHeader ( http . StatusOK )
}
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 )
}
2022-03-03 14:54:37 +01:00
if user := auth . GetUser ( r . Context ( ) ) ; ! user . HasRole ( auth . 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
}
2022-07-07 14:08:37 +02:00
users , err := api . Authentication . 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-01-30 17:01:11 +01:00
func ( api * RestApi ) getRoles ( 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-01-30 17:01:11 +01:00
user := auth . GetUser ( r . Context ( ) )
2023-02-17 15:45:31 +01:00
if ! user . HasRole ( auth . RoleAdmin ) {
2023-01-30 17:01:11 +01:00
http . Error ( rw , "only admins are allowed to fetch a list of roles" , http . StatusForbidden )
return
}
roles , err := auth . GetValidRoles ( user )
if err != nil {
http . Error ( rw , err . Error ( ) , http . StatusInternalServerError )
return
}
json . NewEncoder ( rw ) . Encode ( roles )
}
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 )
}
2022-04-11 12:29:24 +02:00
if user := auth . GetUser ( r . Context ( ) ) ; ! user . HasRole ( auth . 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 != "" {
2022-08-26 15:15:36 +02:00
if err := api . Authentication . AddRole ( r . Context ( ) , mux . Vars ( r ) [ "id" ] , newrole ) ; err != nil {
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 != "" {
2022-08-26 15:15:36 +02:00
if err := api . Authentication . RemoveRole ( r . Context ( ) , mux . Vars ( r ) [ "id" ] , delrole ) ; err != nil {
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-02-17 15:45:31 +01:00
if err := api . Authentication . AddProject ( r . Context ( ) , mux . Vars ( r ) [ "id" ] , newproj ) ; err != nil {
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-02-17 15:45:31 +01:00
if err := api . Authentication . RemoveProject ( r . Context ( ) , mux . Vars ( r ) [ "id" ] , delproj ) ; err != nil {
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-14 14:33:05 +02:00
// func (api *RestApi) secureUpdateUser(rw http.ResponseWriter, r *http.Request) {
// if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
// handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
// return
// }
//
// // IP CHECK HERE (WIP)
// // Probably better as private routine
// IPAddress := r.Header.Get("X-Real-Ip")
// if IPAddress == "" {
// IPAddress = r.Header.Get("X-Forwarded-For")
// }
// if IPAddress == "" {
// IPAddress = r.RemoteAddr
// }
//
// // Also This
// ipOk := false
// for _, a := range config.Keys.ApiAllowedAddrs {
// if a == IPAddress {
// ipOk = true
// }
// }
//
// if IPAddress == "" || ipOk == false {
// handleError(fmt.Errorf("unknown ip: %v", IPAddress), http.StatusForbidden, rw)
// return
// }
// // IP CHECK END
//
// // Get Values
// id := mux.Vars(r)["id"]
// newproj := mux.Vars(r)["project"]
// newrole := mux.Vars(r)["role"]
//
// // TODO: Handle anything but roles...
// if newrole != "" {
// if err := api.Authentication.AddRole(r.Context(), id, newrole); err != nil {
// handleError(errors.New(err.Error()), http.StatusUnprocessableEntity, rw)
// return
// }
//
// rw.Header().Add("Content-Type", "application/json")
// rw.WriteHeader(http.StatusOK)
// json.NewEncoder(rw).Encode(UpdateUserApiResponse{
// Message: fmt.Sprintf("Successfully added role %s to %s", newrole, id),
// })
//
// } else if newproj != "" {
// if err := api.Authentication.AddProject(r.Context(), id, newproj); err != nil {
// handleError(errors.New(err.Error()), http.StatusUnprocessableEntity, rw)
// return
// }
//
// rw.Header().Add("Content-Type", "application/json")
// rw.WriteHeader(http.StatusOK)
// json.NewEncoder(rw).Encode(UpdateUserApiResponse{
// Message: fmt.Sprintf("Successfully added project %s to %s", newproj, id),
// })
//
// } else {
// handleError(errors.New("Not Add [role|project]?"), http.StatusBadRequest, rw)
// }
// }
2023-08-03 17:47:09 +02:00
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
2022-09-12 13:34:21 +02:00
if err := repository . GetUserCfgRepo ( ) . UpdateConfig ( key , value , auth . GetUser ( 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 )
}