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.
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/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-08-30 15:04:50 +02:00
func ( r * JobRepository ) QueryJobs (
ctx context . Context ,
2022-02-17 09:04:57 +01:00
filters [ ] * model . JobFilter ,
page * model . PageRequest ,
order * model . OrderByInput ) ( [ ] * schema . Job , error ) {
2023-08-30 15:04:50 +02:00
query , qerr := SecurityCheck ( ctx , sq . Select ( jobColumns ... ) . From ( "job" ) )
if qerr != nil {
return nil , qerr
}
2022-02-17 09:04:57 +01:00
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 )
}
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-06-20 15:52:16 +02:00
log . Errorf ( "Error while running query: %v" , err )
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-08-30 15:04:50 +02:00
func ( r * JobRepository ) CountJobs (
2023-04-28 12:34:40 +02:00
ctx context . Context ,
2023-08-30 15:04:50 +02:00
filters [ ] * model . JobFilter ) ( int , error ) {
2023-04-28 12:34:40 +02:00
2023-08-30 15:04:50 +02:00
query , qerr := SecurityCheck ( ctx , sq . Select ( "count(*)" ) . From ( "job" ) )
2023-04-28 12:34:40 +02:00
if qerr != nil {
2023-08-30 15:04:50 +02:00
return 0 , qerr
2023-04-28 12:34:40 +02:00
}
2022-02-17 09:04:57 +01:00
for _ , f := range filters {
query = BuildWhereClause ( f , query )
}
2023-06-01 13:57:35 +02:00
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-20 12:54:26 +02:00
func SecurityCheck ( ctx context . Context , query sq . SelectBuilder ) ( sq . SelectBuilder , error ) {
2023-08-17 10:29:00 +02:00
user := GetUserFromContext ( ctx )
2023-06-12 11:35:16 +02:00
if user == nil {
var qnil sq . SelectBuilder
2023-12-08 12:03:04 +01:00
return qnil , fmt . Errorf ( "user context is nil" )
2023-08-17 10:29:00 +02:00
} else if user . HasAnyRole ( [ ] schema . Role { schema . RoleAdmin , schema . RoleSupport , schema . RoleApi } ) { // Admin & Co. : All jobs
2023-01-27 18:36:58 +01:00
return query , nil
2023-08-17 10:29:00 +02:00
} else if user . HasRole ( schema . RoleManager ) { // Manager : Add filter for managed projects' jobs only + personal jobs
2023-02-20 11:24:18 +01:00
if len ( user . Projects ) != 0 {
return query . Where ( sq . Or { sq . Eq { "job.project" : user . Projects } , sq . Eq { "job.user" : user . Username } } ) , nil
} else {
2023-06-20 15:47:38 +02:00
log . Debugf ( "Manager-User '%s' has no defined projects to lookup! Query only personal jobs ..." , user . Username )
2023-02-20 11:24:18 +01:00
return query . Where ( "job.user = ?" , user . Username ) , nil
}
2023-08-17 10:29:00 +02:00
} else if user . HasRole ( schema . RoleUser ) { // User : Only personal jobs
2023-01-27 18:36:58 +01:00
return query . Where ( "job.user = ?" , user . Username ) , nil
2023-06-12 11:35:16 +02:00
} else {
// Shortterm compatibility: Return User-Query if no roles:
return query . Where ( "job.user = ?" , user . Username ) , nil
// // On the longterm: Return Error instead of fallback:
// var qnil sq.SelectBuilder
// 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 {
2024-04-22 12:14:40 +02:00
query = buildMetaJsonCondition ( "jobName" , filter . JobName , query )
2023-01-11 16:25:02 +01:00
}
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 )
}
2023-06-28 13:35:41 +02:00
if filter . Node != nil {
query = buildStringCondition ( "job.resources" , filter . Node , query )
}
2022-02-17 09:04:57 +01:00
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 )
}
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 {
2023-06-30 16:55:34 +02:00
queryElements := make ( [ ] string , len ( cond . In ) )
2023-02-20 10:20:08 +01:00
for i , val := range cond . In {
2023-06-30 16:55:34 +02:00
queryElements [ i ] = val
2023-02-20 10:20:08 +01:00
}
2023-06-30 16:55:34 +02:00
return query . Where ( sq . Or { sq . Eq { field : queryElements } } )
2023-02-20 10:20:08 +01:00
}
2022-02-17 09:04:57 +01:00
return query
}
2024-04-22 12:14:40 +02:00
func buildMetaJsonCondition ( jsonField string , cond * model . StringInput , query sq . SelectBuilder ) sq . SelectBuilder {
if cond . Eq != nil {
return query . Where ( "JSON_EXTRACT(meta_data, \"$." + jsonField + "\") = ?" , * cond . Eq )
}
if cond . Neq != nil {
return query . Where ( "JSON_EXTRACT(meta_data, \"$." + jsonField + "\") != ?" , * cond . Neq )
}
if cond . StartsWith != nil {
return query . Where ( "JSON_EXTRACT(meta_data, \"$." + jsonField + "\") LIKE ?" , fmt . Sprint ( * cond . StartsWith , "%" ) )
}
if cond . EndsWith != nil {
return query . Where ( "JSON_EXTRACT(meta_data, \"$." + jsonField + "\") LIKE ?" , fmt . Sprint ( "%" , * cond . EndsWith ) )
}
if cond . Contains != nil {
return query . Where ( "JSON_EXTRACT(meta_data, \"$." + jsonField + "\") LIKE ?" , fmt . Sprint ( "%" , * cond . Contains , "%" ) )
}
return query
}
2022-02-17 09:04:57 +01:00
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 )
}