2022-07-29 06:29:21 +02:00
// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2022-02-17 09:04:57 +01:00
package repository
import (
"context"
"errors"
"fmt"
"regexp"
"strings"
2022-03-01 16:00:44 +01:00
"time"
2022-02-17 09:04:57 +01:00
2022-06-21 17:52:36 +02:00
"github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
2022-02-17 09:04:57 +01:00
sq "github.com/Masterminds/squirrel"
)
2023-06-01 17:48:43 +02:00
// SecurityCheck-less, private: Returns a list of jobs matching the provided filters. page and order are optional-
2023-06-01 13:57:35 +02:00
func ( r * JobRepository ) queryJobs (
query sq . SelectBuilder ,
2022-02-17 09:04:57 +01:00
filters [ ] * model . JobFilter ,
page * model . PageRequest ,
order * model . OrderByInput ) ( [ ] * schema . Job , error ) {
if order != nil {
field := toSnakeCase ( order . Field )
2023-05-04 15:34:36 +02:00
switch order . Order {
case model . SortDirectionEnumAsc :
2022-02-17 09:04:57 +01:00
query = query . OrderBy ( fmt . Sprintf ( "job.%s ASC" , field ) )
2023-05-04 15:34:36 +02:00
case model . SortDirectionEnumDesc :
2022-02-17 09:04:57 +01:00
query = query . OrderBy ( fmt . Sprintf ( "job.%s DESC" , field ) )
2023-05-04 15:34:36 +02:00
default :
2023-01-19 16:59:14 +01:00
return nil , errors . New ( "REPOSITORY/QUERY > invalid sorting order" )
2022-02-17 09:04:57 +01:00
}
}
2022-02-22 14:07:49 +01:00
if page != nil && page . ItemsPerPage != - 1 {
2022-02-17 09:04:57 +01:00
limit := uint64 ( page . ItemsPerPage )
query = query . Offset ( ( uint64 ( page . Page ) - 1 ) * limit ) . Limit ( limit )
}
for _ , f := range filters {
query = BuildWhereClause ( f , query )
}
sql , args , err := query . ToSql ( )
if err != nil {
2023-02-01 11:58:27 +01:00
log . Warn ( "Error while converting query to sql" )
2022-02-17 09:04:57 +01:00
return nil , err
}
log . Debugf ( "SQL query: `%s`, args: %#v" , sql , args )
2022-02-22 09:25:41 +01:00
rows , err := query . RunWith ( r . stmtCache ) . Query ( )
2022-02-17 09:04:57 +01:00
if err != nil {
2023-01-31 18:28:44 +01:00
log . Error ( "Error while running query" )
2022-02-17 09:04:57 +01:00
return nil , err
}
jobs := make ( [ ] * schema . Job , 0 , 50 )
for rows . Next ( ) {
2022-02-22 09:25:41 +01:00
job , err := scanJob ( rows )
2022-02-17 09:04:57 +01:00
if err != nil {
2022-03-03 14:54:37 +01:00
rows . Close ( )
2023-04-28 12:34:40 +02:00
log . Warn ( "Error while scanning rows (Jobs)" )
2022-02-17 09:04:57 +01:00
return nil , err
}
jobs = append ( jobs , job )
}
return jobs , nil
}
2023-06-01 17:48:43 +02:00
// testFunction for queryJobs
2023-06-01 13:57:35 +02:00
func ( r * JobRepository ) testQueryJobs (
filters [ ] * model . JobFilter ,
page * model . PageRequest ,
order * model . OrderByInput ) ( [ ] * schema . Job , error ) {
return r . queryJobs ( sq . Select ( jobColumns ... ) . From ( "job" ) ,
filters , page , order )
}
2023-06-01 17:48:43 +02:00
// Public function with added securityCheck, calls private queryJobs function above
2023-06-01 13:57:35 +02:00
func ( r * JobRepository ) QueryJobs (
2023-04-28 12:34:40 +02:00
ctx context . Context ,
2023-06-01 13:57:35 +02:00
filters [ ] * model . JobFilter ,
page * model . PageRequest ,
order * model . OrderByInput ) ( [ ] * schema . Job , error ) {
2023-04-28 12:34:40 +02:00
2023-06-01 13:57:35 +02:00
query , qerr := SecurityCheck ( ctx , sq . Select ( jobColumns ... ) . From ( "job" ) )
2023-04-28 12:34:40 +02:00
if qerr != nil {
return nil , qerr
}
2023-06-01 13:57:35 +02:00
return r . queryJobs ( query ,
filters , page , order )
}
2023-06-01 17:48:43 +02:00
// SecurityCheck-less, private: returns a list of minimal job information (DB-ID and jobId) of shared jobs for link-building based the provided filters.
func ( r * JobRepository ) queryJobLinks (
query sq . SelectBuilder ,
filters [ ] * model . JobFilter ) ( [ ] * model . JobLink , error ) {
2023-04-28 12:34:40 +02:00
for _ , f := range filters {
query = BuildWhereClause ( f , query )
}
sql , args , err := query . ToSql ( )
if err != nil {
log . Warn ( "Error while converting query to sql" )
return nil , err
}
log . Debugf ( "SQL query: `%s`, args: %#v" , sql , args )
rows , err := query . RunWith ( r . stmtCache ) . Query ( )
if err != nil {
log . Error ( "Error while running query" )
return nil , err
}
jobLinks := make ( [ ] * model . JobLink , 0 , 50 )
for rows . Next ( ) {
jobLink , err := scanJobLink ( rows )
if err != nil {
rows . Close ( )
log . Warn ( "Error while scanning rows (JobLinks)" )
return nil , err
}
jobLinks = append ( jobLinks , jobLink )
}
return jobLinks , nil
}
2023-06-01 17:48:43 +02:00
// testFunction for queryJobLinks
func ( r * JobRepository ) testQueryJobLinks (
filters [ ] * model . JobFilter ) ( [ ] * model . JobLink , error ) {
return r . queryJobLinks ( sq . Select ( jobColumns ... ) . From ( "job" ) , filters )
}
func ( r * JobRepository ) QueryJobLinks (
2022-02-17 09:04:57 +01:00
ctx context . Context ,
2023-06-01 17:48:43 +02:00
filters [ ] * model . JobFilter ) ( [ ] * model . JobLink , error ) {
2022-02-17 09:04:57 +01:00
2023-06-01 17:48:43 +02:00
query , qerr := SecurityCheck ( ctx , sq . Select ( "job.id" , "job.job_id" ) . From ( "job" ) )
2023-01-27 18:36:58 +01:00
if qerr != nil {
2023-06-01 17:48:43 +02:00
return nil , qerr
2023-01-27 18:36:58 +01:00
}
2023-06-01 17:48:43 +02:00
return r . queryJobLinks ( query , filters )
}
// SecurityCheck-less, private: Returns the number of jobs matching the filters
2023-06-01 13:57:35 +02:00
func ( r * JobRepository ) countJobs ( query sq . SelectBuilder ,
filters [ ] * model . JobFilter ) ( int , error ) {
2022-02-17 09:04:57 +01:00
for _ , f := range filters {
query = BuildWhereClause ( f , query )
}
2023-06-01 13:57:35 +02:00
sql , args , err := query . ToSql ( )
if err != nil {
log . Warn ( "Error while converting query to sql" )
return 0 , nil
}
log . Debugf ( "SQL query: `%s`, args: %#v" , sql , args )
2022-02-17 09:04:57 +01:00
var count int
if err := query . RunWith ( r . DB ) . Scan ( & count ) ; err != nil {
return 0 , err
}
return count , nil
}
2023-06-01 17:48:43 +02:00
// testFunction for countJobs
2023-06-01 13:57:35 +02:00
func ( r * JobRepository ) testCountJobs (
filters [ ] * model . JobFilter ) ( int , error ) {
return r . countJobs ( sq . Select ( "count(*)" ) . From ( "job" ) , filters )
}
2023-06-01 17:48:43 +02:00
// Public function with added securityCheck, calls private countJobs function above
2023-06-01 13:57:35 +02:00
func ( r * JobRepository ) CountJobs (
ctx context . Context ,
filters [ ] * model . JobFilter ) ( int , error ) {
query , qerr := SecurityCheck ( ctx , sq . Select ( "count(*)" ) . From ( "job" ) )
if qerr != nil {
return 0 , qerr
}
return r . countJobs ( query , filters )
}
2023-01-27 18:36:58 +01:00
func SecurityCheck ( ctx context . Context , query sq . SelectBuilder ) ( queryOut sq . SelectBuilder , err error ) {
2022-02-17 09:04:57 +01:00
user := auth . GetUser ( ctx )
2023-03-06 11:44:38 +01:00
if user == nil || user . HasAnyRole ( [ ] auth . Role { auth . RoleAdmin , auth . RoleSupport , auth . RoleApi } ) { // Admin & Co. : All jobs
2023-01-27 18:36:58 +01:00
return query , nil
2023-02-20 11:24:18 +01:00
} else if user . HasRole ( auth . RoleManager ) { // Manager : Add filter for managed projects' jobs only + personal jobs
if len ( user . Projects ) != 0 {
return query . Where ( sq . Or { sq . Eq { "job.project" : user . Projects } , sq . Eq { "job.user" : user . Username } } ) , nil
} else {
log . Infof ( "Manager-User '%s' has no defined projects to lookup! Query only personal jobs ..." , user . Username )
return query . Where ( "job.user = ?" , user . Username ) , nil
}
} else if user . HasRole ( auth . RoleUser ) { // User : Only personal jobs
2023-01-27 18:36:58 +01:00
return query . Where ( "job.user = ?" , user . Username ) , nil
2023-02-20 11:24:18 +01:00
} else { // Unauthorized : Error
2023-01-27 18:36:58 +01:00
var qnil sq . SelectBuilder
2023-05-04 15:34:36 +02:00
return qnil , fmt . Errorf ( "user '%s' with unknown roles [%#v]" , user . Username , user . Roles )
2022-02-17 09:04:57 +01:00
}
}
// Build a sq.SelectBuilder out of a schema.JobFilter.
func BuildWhereClause ( filter * model . JobFilter , query sq . SelectBuilder ) sq . SelectBuilder {
if filter . Tags != nil {
query = query . Join ( "jobtag ON jobtag.job_id = job.id" ) . Where ( sq . Eq { "jobtag.tag_id" : filter . Tags } )
}
if filter . JobID != nil {
query = buildStringCondition ( "job.job_id" , filter . JobID , query )
}
if filter . ArrayJobID != nil {
query = query . Where ( "job.array_job_id = ?" , * filter . ArrayJobID )
}
if filter . User != nil {
query = buildStringCondition ( "job.user" , filter . User , query )
}
if filter . Project != nil {
query = buildStringCondition ( "job.project" , filter . Project , query )
}
2023-01-11 16:25:02 +01:00
if filter . JobName != nil {
query = buildStringCondition ( "job.meta_data" , filter . JobName , query )
}
2022-02-17 09:04:57 +01:00
if filter . Cluster != nil {
query = buildStringCondition ( "job.cluster" , filter . Cluster , query )
}
if filter . Partition != nil {
query = buildStringCondition ( "job.partition" , filter . Partition , query )
}
if filter . StartTime != nil {
query = buildTimeCondition ( "job.start_time" , filter . StartTime , query )
}
if filter . Duration != nil {
2022-03-01 16:00:44 +01:00
now := time . Now ( ) . Unix ( ) // There does not seam to be a portable way to get the current unix timestamp accross different DBs.
2022-03-01 16:04:27 +01:00
query = query . Where ( "(CASE WHEN job.job_state = 'running' THEN (? - job.start_time) ELSE job.duration END) BETWEEN ? AND ?" , now , filter . Duration . From , filter . Duration . To )
2022-02-17 09:04:57 +01:00
}
2022-03-02 10:48:52 +01:00
if filter . MinRunningFor != nil {
now := time . Now ( ) . Unix ( ) // There does not seam to be a portable way to get the current unix timestamp accross different DBs.
query = query . Where ( "(job.job_state != 'running' OR (? - job.start_time) > ?)" , now , * filter . MinRunningFor )
}
2022-02-17 09:04:57 +01:00
if filter . State != nil {
states := make ( [ ] string , len ( filter . State ) )
for i , val := range filter . State {
states [ i ] = string ( val )
}
query = query . Where ( sq . Eq { "job.job_state" : states } )
}
if filter . NumNodes != nil {
query = buildIntCondition ( "job.num_nodes" , filter . NumNodes , query )
}
if filter . NumAccelerators != nil {
query = buildIntCondition ( "job.num_acc" , filter . NumAccelerators , query )
}
if filter . NumHWThreads != nil {
query = buildIntCondition ( "job.num_hwthreads" , filter . NumHWThreads , query )
}
if filter . FlopsAnyAvg != nil {
query = buildFloatCondition ( "job.flops_any_avg" , filter . FlopsAnyAvg , query )
}
if filter . MemBwAvg != nil {
query = buildFloatCondition ( "job.mem_bw_avg" , filter . MemBwAvg , query )
}
if filter . LoadAvg != nil {
query = buildFloatCondition ( "job.load_avg" , filter . LoadAvg , query )
}
if filter . MemUsedMax != nil {
query = buildFloatCondition ( "job.mem_used_max" , filter . MemUsedMax , query )
}
2023-04-28 12:34:40 +02:00
// Shared Jobs Query
if filter . Exclusive != nil {
query = query . Where ( "job.exclusive = ?" , * filter . Exclusive )
}
if filter . SharedNode != nil {
query = buildStringCondition ( "job.resources" , filter . SharedNode , query )
}
if filter . SelfJobID != nil {
query = buildStringCondition ( "job.job_id" , filter . SelfJobID , query )
}
2023-05-16 12:42:06 +02:00
if filter . SelfStartTime != nil && filter . SelfDuration != nil {
start := filter . SelfStartTime . Unix ( ) + 10 // There does not seem to be a portable way to get the current unix timestamp accross different DBs.
end := start + int64 ( * filter . SelfDuration ) - 20
2023-04-28 12:34:40 +02:00
query = query . Where ( "((job.start_time BETWEEN ? AND ?) OR ((job.start_time + job.duration) BETWEEN ? AND ?))" , start , end , start , end )
}
2022-02-17 09:04:57 +01:00
return query
}
2022-09-07 12:24:45 +02:00
func buildIntCondition ( field string , cond * schema . IntRange , query sq . SelectBuilder ) sq . SelectBuilder {
2022-02-17 09:04:57 +01:00
return query . Where ( field + " BETWEEN ? AND ?" , cond . From , cond . To )
}
2022-09-07 12:24:45 +02:00
func buildTimeCondition ( field string , cond * schema . TimeRange , query sq . SelectBuilder ) sq . SelectBuilder {
2022-02-17 09:04:57 +01:00
if cond . From != nil && cond . To != nil {
return query . Where ( field + " BETWEEN ? AND ?" , cond . From . Unix ( ) , cond . To . Unix ( ) )
} else if cond . From != nil {
return query . Where ( "? <= " + field , cond . From . Unix ( ) )
} else if cond . To != nil {
return query . Where ( field + " <= ?" , cond . To . Unix ( ) )
} else {
return query
}
}
func buildFloatCondition ( field string , cond * model . FloatRange , query sq . SelectBuilder ) sq . SelectBuilder {
return query . Where ( field + " BETWEEN ? AND ?" , cond . From , cond . To )
}
func buildStringCondition ( field string , cond * model . StringInput , query sq . SelectBuilder ) sq . SelectBuilder {
if cond . Eq != nil {
return query . Where ( field + " = ?" , * cond . Eq )
}
2023-04-28 12:34:40 +02:00
if cond . Neq != nil {
return query . Where ( field + " != ?" , * cond . Neq )
}
2022-02-17 09:04:57 +01:00
if cond . StartsWith != nil {
return query . Where ( field + " LIKE ?" , fmt . Sprint ( * cond . StartsWith , "%" ) )
}
if cond . EndsWith != nil {
return query . Where ( field + " LIKE ?" , fmt . Sprint ( "%" , * cond . EndsWith ) )
}
if cond . Contains != nil {
return query . Where ( field + " LIKE ?" , fmt . Sprint ( "%" , * cond . Contains , "%" ) )
}
2023-02-20 10:20:08 +01:00
if cond . In != nil {
queryUsers := make ( [ ] string , len ( cond . In ) )
for i , val := range cond . In {
queryUsers [ i ] = val
}
return query . Where ( sq . Or { sq . Eq { "job.user" : queryUsers } } )
}
2022-02-17 09:04:57 +01:00
return query
}
var matchFirstCap = regexp . MustCompile ( "(.)([A-Z][a-z]+)" )
var matchAllCap = regexp . MustCompile ( "([a-z0-9])([A-Z])" )
func toSnakeCase ( str string ) string {
for _ , c := range str {
if c == '\'' || c == '\\' {
2023-01-23 18:48:06 +01:00
log . Panic ( "toSnakeCase() attack vector!" )
2022-02-17 09:04:57 +01:00
}
}
str = strings . ReplaceAll ( str , "'" , "" )
str = strings . ReplaceAll ( str , "\\" , "" )
snake := matchFirstCap . ReplaceAllString ( str , "${1}_${2}" )
snake = matchAllCap . ReplaceAllString ( snake , "${1}_${2}" )
return strings . ToLower ( snake )
}