cleanup and comments

This commit is contained in:
Lou Knauer 2022-03-15 08:29:29 +01:00
parent f69c3945d4
commit 7be38277a9
8 changed files with 210 additions and 223 deletions

View File

@ -14,6 +14,4 @@ jobs:
run: | run: |
go build ./... go build ./...
go vet ./... go vet ./...
go test . go test ./...
env BASEPATH="../" go test ./repository
env BASEPATH="../" go test ./config

View File

@ -1,4 +1,4 @@
package main package repository
import ( import (
"bufio" "bufio"
@ -9,14 +9,13 @@ import (
"time" "time"
"github.com/ClusterCockpit/cc-backend/log" "github.com/ClusterCockpit/cc-backend/log"
"github.com/ClusterCockpit/cc-backend/repository"
"github.com/ClusterCockpit/cc-backend/schema" "github.com/ClusterCockpit/cc-backend/schema"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
// `AUTO_INCREMENT` is in a comment because of this hack: // `AUTO_INCREMENT` is in a comment because of this hack:
// https://stackoverflow.com/a/41028314 (sqlite creates unique ids automatically) // https://stackoverflow.com/a/41028314 (sqlite creates unique ids automatically)
const JOBS_DB_SCHEMA string = ` const JobsDBSchema string = `
DROP TABLE IF EXISTS jobtag; DROP TABLE IF EXISTS jobtag;
DROP TABLE IF EXISTS job; DROP TABLE IF EXISTS job;
DROP TABLE IF EXISTS tag; DROP TABLE IF EXISTS tag;
@ -32,8 +31,8 @@ const JOBS_DB_SCHEMA string = `
project VARCHAR(255) NOT NULL, project VARCHAR(255) NOT NULL,
` + "`partition`" + ` VARCHAR(255) NOT NULL, -- partition is a keyword in mysql -.- ` + "`partition`" + ` VARCHAR(255) NOT NULL, -- partition is a keyword in mysql -.-
array_job_id BIGINT NOT NULL, array_job_id BIGINT NOT NULL,
duration INT, duration INT NOT NULL DEFAULT 0,
walltime INT, walltime INT NOT NULL DEFAULT 0,
job_state VARCHAR(255) NOT NULL CHECK(job_state IN ('running', 'completed', 'failed', 'cancelled', 'stopped', 'timeout', 'preempted', 'out_of_memory')), job_state VARCHAR(255) NOT NULL CHECK(job_state IN ('running', 'completed', 'failed', 'cancelled', 'stopped', 'timeout', 'preempted', 'out_of_memory')),
meta_data TEXT, -- JSON meta_data TEXT, -- JSON
resources TEXT NOT NULL, -- JSON resources TEXT NOT NULL, -- JSON
@ -68,7 +67,8 @@ const JOBS_DB_SCHEMA string = `
FOREIGN KEY (tag_id) REFERENCES tag (id) ON DELETE CASCADE); FOREIGN KEY (tag_id) REFERENCES tag (id) ON DELETE CASCADE);
` `
const JOBS_DB_INDEXES string = ` // Indexes are created after the job-archive is traversed for faster inserts.
const JobsDbIndexes string = `
CREATE INDEX job_by_user ON job (user); CREATE INDEX job_by_user ON job (user);
CREATE INDEX job_by_starttime ON job (start_time); CREATE INDEX job_by_starttime ON job (start_time);
CREATE INDEX job_by_job_id ON job (job_id); CREATE INDEX job_by_job_id ON job (job_id);
@ -77,12 +77,12 @@ const JOBS_DB_INDEXES string = `
// Delete the tables "job", "tag" and "jobtag" from the database and // Delete the tables "job", "tag" and "jobtag" from the database and
// repopulate them using the jobs found in `archive`. // repopulate them using the jobs found in `archive`.
func initDB(db *sqlx.DB, archive string) error { func InitDB(db *sqlx.DB, archive string) error {
starttime := time.Now() starttime := time.Now()
log.Print("Building job table...") log.Print("Building job table...")
// Basic database structure: // Basic database structure:
_, err := db.Exec(JOBS_DB_SCHEMA) _, err := db.Exec(JobsDBSchema)
if err != nil { if err != nil {
return err return err
} }
@ -96,16 +96,21 @@ func initDB(db *sqlx.DB, archive string) error {
return err return err
} }
// Inserts are bundled into transactions because in sqlite,
// that speeds up inserts A LOT.
tx, err := db.Beginx() tx, err := db.Beginx()
if err != nil { if err != nil {
return err return err
} }
stmt, err := tx.PrepareNamed(repository.NamedJobInsert) stmt, err := tx.PrepareNamed(NamedJobInsert)
if err != nil { if err != nil {
return err return err
} }
// Not using log.Print because we want the line to end with `\r` and
// this function is only ever called when a special command line flag
// is passed anyways.
fmt.Printf("%d jobs inserted...\r", 0) fmt.Printf("%d jobs inserted...\r", 0)
i := 0 i := 0
tags := make(map[string]int64) tags := make(map[string]int64)
@ -159,6 +164,8 @@ func initDB(db *sqlx.DB, archive string) error {
return err return err
} }
// For compability with the old job-archive directory structure where
// there was no start time directory.
for _, startTimeDir := range startTimeDirs { for _, startTimeDir := range startTimeDirs {
if startTimeDir.Type().IsRegular() && startTimeDir.Name() == "meta.json" { if startTimeDir.Type().IsRegular() && startTimeDir.Name() == "meta.json" {
if err := handleDirectory(dirpath); err != nil { if err := handleDirectory(dirpath); err != nil {
@ -180,7 +187,7 @@ func initDB(db *sqlx.DB, archive string) error {
// Create indexes after inserts so that they do not // Create indexes after inserts so that they do not
// need to be continually updated. // need to be continually updated.
if _, err := db.Exec(JOBS_DB_INDEXES); err != nil { if _, err := db.Exec(JobsDbIndexes); err != nil {
return err return err
} }
@ -226,7 +233,7 @@ func loadJob(tx *sqlx.Tx, stmt *sqlx.NamedStmt, tags map[string]int64, path stri
return err return err
} }
if err := repository.SanityChecks(&job.BaseJob); err != nil { if err := SanityChecks(&job.BaseJob); err != nil {
return err return err
} }
@ -262,11 +269,3 @@ func loadJob(tx *sqlx.Tx, stmt *sqlx.NamedStmt, tags map[string]int64, path stri
return nil return nil
} }
func loadJobStat(job *schema.JobMeta, metric string) float64 {
if stats, ok := job.Statistics[metric]; ok {
return stats.Avg
}
return 0.0
}

View File

@ -5,14 +5,17 @@ import (
"testing" "testing"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"github.com/ClusterCockpit/cc-backend/test"
) )
var db *sqlx.DB var db *sqlx.DB
func init() { func init() {
db = test.InitDB() var err error
db, err = sqlx.Open("sqlite3", "../test/test.db")
if err != nil {
fmt.Println(err)
}
} }
func setup(t *testing.T) *JobRepository { func setup(t *testing.T) *JobRepository {

128
routes.go
View File

@ -9,6 +9,9 @@ import (
"github.com/ClusterCockpit/cc-backend/auth" "github.com/ClusterCockpit/cc-backend/auth"
"github.com/ClusterCockpit/cc-backend/config" "github.com/ClusterCockpit/cc-backend/config"
"github.com/ClusterCockpit/cc-backend/graph"
"github.com/ClusterCockpit/cc-backend/graph/model"
"github.com/ClusterCockpit/cc-backend/log"
"github.com/ClusterCockpit/cc-backend/schema" "github.com/ClusterCockpit/cc-backend/schema"
"github.com/ClusterCockpit/cc-backend/templates" "github.com/ClusterCockpit/cc-backend/templates"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -24,6 +27,131 @@ type Route struct {
Setup func(i InfoType, r *http.Request) InfoType Setup func(i InfoType, r *http.Request) InfoType
} }
var routes []Route = []Route{
{"/", "home.tmpl", "ClusterCockpit", false, setupHomeRoute},
{"/config", "config.tmpl", "Settings", false, func(i InfoType, r *http.Request) InfoType { return i }},
{"/monitoring/jobs/", "monitoring/jobs.tmpl", "Jobs - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { return i }},
{"/monitoring/job/{id:[0-9]+}", "monitoring/job.tmpl", "Job <ID> - ClusterCockpit", false, setupJobRoute},
{"/monitoring/users/", "monitoring/list.tmpl", "Users - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "USER"; return i }},
{"/monitoring/projects/", "monitoring/list.tmpl", "Projects - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "PROJECT"; return i }},
{"/monitoring/tags/", "monitoring/taglist.tmpl", "Tags - ClusterCockpit", false, setupTaglistRoute},
{"/monitoring/user/{id}", "monitoring/user.tmpl", "User <ID> - ClusterCockpit", true, setupUserRoute},
{"/monitoring/systems/{cluster}", "monitoring/systems.tmpl", "Cluster <ID> - ClusterCockpit", false, setupClusterRoute},
{"/monitoring/node/{cluster}/{hostname}", "monitoring/node.tmpl", "Node <ID> - ClusterCockpit", false, setupNodeRoute},
{"/monitoring/analysis/{cluster}", "monitoring/analysis.tmpl", "Analaysis - ClusterCockpit", true, setupAnalysisRoute},
}
func setupHomeRoute(i InfoType, r *http.Request) InfoType {
type cluster struct {
Name string
RunningJobs int
TotalJobs int
RecentShortJobs int
}
runningJobs, err := jobRepo.CountGroupedJobs(r.Context(), model.AggregateCluster, []*model.JobFilter{{
State: []schema.JobState{schema.JobStateRunning},
}}, nil)
if err != nil {
log.Errorf("failed to count jobs: %s", err.Error())
runningJobs = map[string]int{}
}
totalJobs, err := jobRepo.CountGroupedJobs(r.Context(), model.AggregateCluster, nil, nil)
if err != nil {
log.Errorf("failed to count jobs: %s", err.Error())
totalJobs = map[string]int{}
}
from := time.Now().Add(-24 * time.Hour)
recentShortJobs, err := jobRepo.CountGroupedJobs(r.Context(), model.AggregateCluster, []*model.JobFilter{{
StartTime: &model.TimeRange{From: &from, To: nil},
Duration: &model.IntRange{From: 0, To: graph.ShortJobDuration},
}}, nil)
if err != nil {
log.Errorf("failed to count jobs: %s", err.Error())
recentShortJobs = map[string]int{}
}
clusters := make([]cluster, 0)
for _, c := range config.Clusters {
clusters = append(clusters, cluster{
Name: c.Name,
RunningJobs: runningJobs[c.Name],
TotalJobs: totalJobs[c.Name],
RecentShortJobs: recentShortJobs[c.Name],
})
}
i["clusters"] = clusters
return i
}
func setupJobRoute(i InfoType, r *http.Request) InfoType {
i["id"] = mux.Vars(r)["id"]
return i
}
func setupUserRoute(i InfoType, r *http.Request) InfoType {
i["id"] = mux.Vars(r)["id"]
i["username"] = mux.Vars(r)["id"]
return i
}
func setupClusterRoute(i InfoType, r *http.Request) InfoType {
vars := mux.Vars(r)
i["id"] = vars["cluster"]
i["cluster"] = vars["cluster"]
from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to")
if from != "" || to != "" {
i["from"] = from
i["to"] = to
}
return i
}
func setupNodeRoute(i InfoType, r *http.Request) InfoType {
vars := mux.Vars(r)
i["cluster"] = vars["cluster"]
i["hostname"] = vars["hostname"]
from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to")
if from != "" || to != "" {
i["from"] = from
i["to"] = to
}
return i
}
func setupAnalysisRoute(i InfoType, r *http.Request) InfoType {
i["cluster"] = mux.Vars(r)["cluster"]
return i
}
func setupTaglistRoute(i InfoType, r *http.Request) InfoType {
var username *string = nil
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleAdmin) {
username = &user.Username
}
tags, counts, err := jobRepo.CountTags(username)
tagMap := make(map[string][]map[string]interface{})
if err != nil {
log.Errorf("GetTags failed: %s", err.Error())
i["tagmap"] = tagMap
return i
}
for _, tag := range tags {
tagItem := map[string]interface{}{
"id": tag.ID,
"name": tag.Name,
"count": counts[tag.Name],
}
tagMap[tag.Type] = append(tagMap[tag.Type], tagItem)
}
i["tagmap"] = tagMap
return i
}
func buildFilterPresets(query url.Values) map[string]interface{} { func buildFilterPresets(query url.Values) map[string]interface{} {
filterPresets := map[string]interface{}{} filterPresets := map[string]interface{}{}

View File

@ -12,6 +12,9 @@ import (
"syscall" "syscall"
) )
// Very simple and limited .env file reader.
// All variable definitions found are directly
// added to the processes environment.
func loadEnv(file string) error { func loadEnv(file string) error {
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
@ -74,6 +77,10 @@ func loadEnv(file string) error {
return s.Err() return s.Err()
} }
// Changes the processes user and group to that
// specified in the config.json. The go runtime
// takes care of all threads (and not only the calling one)
// executing the underlying systemcall.
func dropPrivileges() error { func dropPrivileges() error {
if programConfig.Group != "" { if programConfig.Group != "" {
g, err := user.LookupGroup(programConfig.Group) g, err := user.LookupGroup(programConfig.Group)

208
server.go
View File

@ -25,11 +25,9 @@ import (
"github.com/ClusterCockpit/cc-backend/config" "github.com/ClusterCockpit/cc-backend/config"
"github.com/ClusterCockpit/cc-backend/graph" "github.com/ClusterCockpit/cc-backend/graph"
"github.com/ClusterCockpit/cc-backend/graph/generated" "github.com/ClusterCockpit/cc-backend/graph/generated"
"github.com/ClusterCockpit/cc-backend/graph/model"
"github.com/ClusterCockpit/cc-backend/log" "github.com/ClusterCockpit/cc-backend/log"
"github.com/ClusterCockpit/cc-backend/metricdata" "github.com/ClusterCockpit/cc-backend/metricdata"
"github.com/ClusterCockpit/cc-backend/repository" "github.com/ClusterCockpit/cc-backend/repository"
"github.com/ClusterCockpit/cc-backend/schema"
"github.com/ClusterCockpit/cc-backend/templates" "github.com/ClusterCockpit/cc-backend/templates"
"github.com/google/gops/agent" "github.com/google/gops/agent"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
@ -40,7 +38,6 @@ import (
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
var db *sqlx.DB
var jobRepo *repository.JobRepository var jobRepo *repository.JobRepository
// Format of the configurartion (file). See below for the defaults. // Format of the configurartion (file). See below for the defaults.
@ -127,147 +124,22 @@ var programConfig ProgramConfig = ProgramConfig{
}, },
} }
func setupHomeRoute(i InfoType, r *http.Request) InfoType {
type cluster struct {
Name string
RunningJobs int
TotalJobs int
RecentShortJobs int
}
runningJobs, err := jobRepo.CountGroupedJobs(r.Context(), model.AggregateCluster, []*model.JobFilter{{
State: []schema.JobState{schema.JobStateRunning},
}}, nil)
if err != nil {
log.Errorf("failed to count jobs: %s", err.Error())
runningJobs = map[string]int{}
}
totalJobs, err := jobRepo.CountGroupedJobs(r.Context(), model.AggregateCluster, nil, nil)
if err != nil {
log.Errorf("failed to count jobs: %s", err.Error())
totalJobs = map[string]int{}
}
from := time.Now().Add(-24 * time.Hour)
recentShortJobs, err := jobRepo.CountGroupedJobs(r.Context(), model.AggregateCluster, []*model.JobFilter{{
StartTime: &model.TimeRange{From: &from, To: nil},
Duration: &model.IntRange{From: 0, To: graph.ShortJobDuration},
}}, nil)
if err != nil {
log.Errorf("failed to count jobs: %s", err.Error())
recentShortJobs = map[string]int{}
}
clusters := make([]cluster, 0)
for _, c := range config.Clusters {
clusters = append(clusters, cluster{
Name: c.Name,
RunningJobs: runningJobs[c.Name],
TotalJobs: totalJobs[c.Name],
RecentShortJobs: recentShortJobs[c.Name],
})
}
i["clusters"] = clusters
return i
}
func setupJobRoute(i InfoType, r *http.Request) InfoType {
i["id"] = mux.Vars(r)["id"]
return i
}
func setupUserRoute(i InfoType, r *http.Request) InfoType {
i["id"] = mux.Vars(r)["id"]
i["username"] = mux.Vars(r)["id"]
return i
}
func setupClusterRoute(i InfoType, r *http.Request) InfoType {
vars := mux.Vars(r)
i["id"] = vars["cluster"]
i["cluster"] = vars["cluster"]
from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to")
if from != "" || to != "" {
i["from"] = from
i["to"] = to
}
return i
}
func setupNodeRoute(i InfoType, r *http.Request) InfoType {
vars := mux.Vars(r)
i["cluster"] = vars["cluster"]
i["hostname"] = vars["hostname"]
from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to")
if from != "" || to != "" {
i["from"] = from
i["to"] = to
}
return i
}
func setupAnalysisRoute(i InfoType, r *http.Request) InfoType {
i["cluster"] = mux.Vars(r)["cluster"]
return i
}
func setupTaglistRoute(i InfoType, r *http.Request) InfoType {
var username *string = nil
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleAdmin) {
username = &user.Username
}
tags, counts, err := jobRepo.CountTags(username)
tagMap := make(map[string][]map[string]interface{})
if err != nil {
log.Errorf("GetTags failed: %s", err.Error())
i["tagmap"] = tagMap
return i
}
for _, tag := range tags {
tagItem := map[string]interface{}{
"id": tag.ID,
"name": tag.Name,
"count": counts[tag.Name],
}
tagMap[tag.Type] = append(tagMap[tag.Type], tagItem)
}
log.Infof("TAGS %+v", tags)
i["tagmap"] = tagMap
return i
}
var routes []Route = []Route{
{"/", "home.tmpl", "ClusterCockpit", false, setupHomeRoute},
{"/config", "config.tmpl", "Settings", false, func(i InfoType, r *http.Request) InfoType { return i }},
{"/monitoring/jobs/", "monitoring/jobs.tmpl", "Jobs - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { return i }},
{"/monitoring/job/{id:[0-9]+}", "monitoring/job.tmpl", "Job <ID> - ClusterCockpit", false, setupJobRoute},
{"/monitoring/users/", "monitoring/list.tmpl", "Users - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "USER"; return i }},
{"/monitoring/projects/", "monitoring/list.tmpl", "Projects - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "PROJECT"; return i }},
{"/monitoring/tags/", "monitoring/taglist.tmpl", "Tags - ClusterCockpit", false, setupTaglistRoute},
{"/monitoring/user/{id}", "monitoring/user.tmpl", "User <ID> - ClusterCockpit", true, setupUserRoute},
{"/monitoring/systems/{cluster}", "monitoring/systems.tmpl", "Cluster <ID> - ClusterCockpit", false, setupClusterRoute},
{"/monitoring/node/{cluster}/{hostname}", "monitoring/node.tmpl", "Node <ID> - ClusterCockpit", false, setupNodeRoute},
{"/monitoring/analysis/{cluster}", "monitoring/analysis.tmpl", "Analaysis - ClusterCockpit", true, setupAnalysisRoute},
}
func main() { func main() {
var flagReinitDB, flagStopImmediately, flagSyncLDAP, flagGops bool var flagReinitDB, flagStopImmediately, flagSyncLDAP, flagGops bool
var flagConfigFile, flagImportJob string var flagConfigFile, flagImportJob string
var flagNewUser, flagDelUser, flagGenJWT string var flagNewUser, flagDelUser, flagGenJWT string
flag.BoolVar(&flagReinitDB, "init-db", false, "Go through job-archive and re-initialize `job`, `tag`, and `jobtag` tables") flag.BoolVar(&flagReinitDB, "init-db", false, "Go through job-archive and re-initialize the 'job', 'tag', and 'jobtag' tables (all running jobs will be lost!)")
flag.BoolVar(&flagSyncLDAP, "sync-ldap", false, "Sync the `user` table with ldap") flag.BoolVar(&flagSyncLDAP, "sync-ldap", false, "Sync the 'user' table with ldap")
flag.BoolVar(&flagStopImmediately, "no-server", false, "Do not start a server, stop right after initialization and argument handling") flag.BoolVar(&flagStopImmediately, "no-server", false, "Do not start a server, stop right after initialization and argument handling")
flag.BoolVar(&flagGops, "gops", false, "Enable a github.com/google/gops/agent") flag.BoolVar(&flagGops, "gops", false, "Listen via github.com/google/gops/agent (for debugging)")
flag.StringVar(&flagConfigFile, "config", "", "Location of the config file for this server (overwrites the defaults)") flag.StringVar(&flagConfigFile, "config", "", "Overwrite the global config options by those specified in `config.json`")
flag.StringVar(&flagNewUser, "add-user", "", "Add a new user. Argument format: `<username>:[admin,api,user]:<password>`") flag.StringVar(&flagNewUser, "add-user", "", "Add a new user. Argument format: `<username>:[admin,api,user]:<password>`")
flag.StringVar(&flagDelUser, "del-user", "", "Remove user by username") flag.StringVar(&flagDelUser, "del-user", "", "Remove user by `username`")
flag.StringVar(&flagGenJWT, "jwt", "", "Generate and print a JWT for the user specified by the username") flag.StringVar(&flagGenJWT, "jwt", "", "Generate and print a JWT for the user specified by its `username`")
flag.StringVar(&flagImportJob, "import-job", "", "Import a job. Argument format: `<path-to-meta.json>:<path-to-data.json>,...`") flag.StringVar(&flagImportJob, "import-job", "", "Import a job. Argument format: `<path-to-meta.json>:<path-to-data.json>,...`")
flag.Parse() flag.Parse()
// See https://github.com/google/gops (Runtime overhead is almost zero)
if flagGops { if flagGops {
if err := agent.Listen(agent.Options{}); err != nil { if err := agent.Listen(agent.Options{}); err != nil {
log.Fatalf("gops/agent.Listen failed: %s", err.Error()) log.Fatalf("gops/agent.Listen failed: %s", err.Error())
@ -291,18 +163,24 @@ func main() {
} }
} }
// As a special case for `db`, allow using an environment variable instead of the value
// stored in the config. This can be done for people having security concerns about storing
// the password for their mysql database in the config.json.
if strings.HasPrefix(programConfig.DB, "env:") { if strings.HasPrefix(programConfig.DB, "env:") {
envvar := strings.TrimPrefix(programConfig.DB, "env:") envvar := strings.TrimPrefix(programConfig.DB, "env:")
programConfig.DB = os.Getenv(envvar) programConfig.DB = os.Getenv(envvar)
} }
var err error var err error
var db *sqlx.DB
if programConfig.DBDriver == "sqlite3" { if programConfig.DBDriver == "sqlite3" {
db, err = sqlx.Open("sqlite3", fmt.Sprintf("%s?_foreign_keys=on", programConfig.DB)) db, err = sqlx.Open("sqlite3", fmt.Sprintf("%s?_foreign_keys=on", programConfig.DB))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// sqlite does not multithread. Having more than one connection open would just mean
// waiting for locks.
db.SetMaxOpenConns(1) db.SetMaxOpenConns(1)
} else if programConfig.DBDriver == "mysql" { } else if programConfig.DBDriver == "mysql" {
db, err = sqlx.Open("mysql", fmt.Sprintf("%s?multiStatements=true", programConfig.DB)) db, err = sqlx.Open("mysql", fmt.Sprintf("%s?multiStatements=true", programConfig.DB))
@ -317,7 +195,9 @@ func main() {
log.Fatalf("unsupported database driver: %s", programConfig.DBDriver) log.Fatalf("unsupported database driver: %s", programConfig.DBDriver)
} }
// Initialize sub-modules... // Initialize sub-modules and handle all command line flags.
// The order here is important! For example, the metricdata package
// depends on the config package.
var authentication *auth.Authentication var authentication *auth.Authentication
if !programConfig.DisableAuthentication { if !programConfig.DisableAuthentication {
@ -380,7 +260,7 @@ func main() {
} }
if flagReinitDB { if flagReinitDB {
if err := initDB(db, programConfig.JobArchive); err != nil { if err := repository.InitDB(db, programConfig.JobArchive); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
@ -400,11 +280,13 @@ func main() {
return return
} }
// Build routes... // Setup the http.Handler/Router used by the server
resolver := &graph.Resolver{DB: db, Repo: jobRepo} resolver := &graph.Resolver{DB: db, Repo: jobRepo}
graphQLEndpoint := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: resolver})) graphQLEndpoint := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: resolver}))
if os.Getenv("DEBUG") != "1" { if os.Getenv("DEBUG") != "1" {
// Having this handler means that a error message is returned via GraphQL instead of the connection simply beeing closed.
// The problem with this is that then, no more stacktrace is printed to stderr.
graphQLEndpoint.SetRecoverFunc(func(ctx context.Context, err interface{}) error { graphQLEndpoint.SetRecoverFunc(func(ctx context.Context, err interface{}) error {
switch e := err.(type) { switch e := err.(type) {
case string: case string:
@ -417,7 +299,6 @@ func main() {
}) })
} }
graphQLPlayground := playground.Handler("GraphQL playground", "/query")
api := &api.RestApi{ api := &api.RestApi{
JobRepository: jobRepo, JobRepository: jobRepo,
Resolver: resolver, Resolver: resolver,
@ -425,33 +306,21 @@ func main() {
Authentication: authentication, Authentication: authentication,
} }
handleGetLogin := func(rw http.ResponseWriter, r *http.Request) {
templates.Render(rw, r, "login.tmpl", &templates.Page{
Title: "Login",
})
}
r := mux.NewRouter() r := mux.NewRouter()
r.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
templates.Render(rw, r, "404.tmpl", &templates.Page{
Title: "Not found",
})
})
r.Handle("/playground", graphQLPlayground) r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) {
templates.Render(rw, r, "login.tmpl", &templates.Page{Title: "Login"})
r.HandleFunc("/login", handleGetLogin).Methods(http.MethodGet) }).Methods(http.MethodGet)
r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) { r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) {
templates.Render(rw, r, "imprint.tmpl", &templates.Page{ templates.Render(rw, r, "imprint.tmpl", &templates.Page{Title: "Imprint"})
Title: "Imprint",
})
}) })
r.HandleFunc("/privacy", func(rw http.ResponseWriter, r *http.Request) { r.HandleFunc("/privacy", func(rw http.ResponseWriter, r *http.Request) {
templates.Render(rw, r, "privacy.tmpl", &templates.Page{ templates.Render(rw, r, "privacy.tmpl", &templates.Page{Title: "Privacy"})
Title: "Privacy",
})
}) })
// Some routes, such as /login or /query, should only be accessible to a user that is logged in.
// Those should be mounted to this subrouter. If authentication is enabled, a middleware will prevent
// any unauthenticated accesses.
secured := r.PathPrefix("/").Subrouter() secured := r.PathPrefix("/").Subrouter()
if !programConfig.DisableAuthentication { if !programConfig.DisableAuthentication {
r.Handle("/login", authentication.Login( r.Handle("/login", authentication.Login(
@ -490,8 +359,11 @@ func main() {
}) })
}) })
} }
r.Handle("/playground", playground.Handler("GraphQL playground", "/query"))
secured.Handle("/query", graphQLEndpoint) secured.Handle("/query", graphQLEndpoint)
// Send a searchId and then reply with a redirect to a user or job.
secured.HandleFunc("/search", func(rw http.ResponseWriter, r *http.Request) { secured.HandleFunc("/search", func(rw http.ResponseWriter, r *http.Request) {
if search := r.URL.Query().Get("searchId"); search != "" { if search := r.URL.Query().Get("searchId"); search != "" {
job, username, err := api.JobRepository.FindJobOrUser(r.Context(), search) job, username, err := api.JobRepository.FindJobOrUser(r.Context(), search)
@ -515,6 +387,7 @@ func main() {
} }
}) })
// Mount all /monitoring/... and /api/... routes.
setupRoutes(secured, routes) setupRoutes(secured, routes)
api.MountRoutes(secured) api.MountRoutes(secured)
@ -525,11 +398,18 @@ func main() {
handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}), handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}),
handlers.AllowedMethods([]string{"GET", "POST", "HEAD", "OPTIONS"}), handlers.AllowedMethods([]string{"GET", "POST", "HEAD", "OPTIONS"}),
handlers.AllowedOrigins([]string{"*"}))) handlers.AllowedOrigins([]string{"*"})))
handler := handlers.CustomLoggingHandler(log.InfoWriter, r, func(w io.Writer, params handlers.LogFormatterParams) { handler := handlers.CustomLoggingHandler(io.Discard, r, func(_ io.Writer, params handlers.LogFormatterParams) {
log.Finfof(w, "%s %s (%d, %.02fkb, %dms)", if strings.HasPrefix(params.Request.RequestURI, "/api/") {
params.Request.Method, params.URL.RequestURI(), log.Infof("%s %s (%d, %.02fkb, %dms)",
params.StatusCode, float32(params.Size)/1024, params.Request.Method, params.URL.RequestURI(),
time.Since(params.TimeStamp).Milliseconds()) params.StatusCode, float32(params.Size)/1024,
time.Since(params.TimeStamp).Milliseconds())
} else {
log.Debugf("%s %s (%d, %.02fkb, %dms)",
params.Request.Method, params.URL.RequestURI(),
params.StatusCode, float32(params.Size)/1024,
time.Since(params.TimeStamp).Milliseconds())
}
}) })
var wg sync.WaitGroup var wg sync.WaitGroup

View File

@ -1,4 +1,4 @@
package main package test
import ( import (
"bytes" "bytes"
@ -21,13 +21,11 @@ import (
"github.com/ClusterCockpit/cc-backend/schema" "github.com/ClusterCockpit/cc-backend/schema"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
) )
func setup(t *testing.T) *api.RestApi { func setup(t *testing.T) *api.RestApi {
if db != nil {
panic("prefer using sub-tests (`t.Run`) or implement `cleanup` before calling setup twice.")
}
const testclusterJson = `{ const testclusterJson = `{
"name": "testcluster", "name": "testcluster",
"subClusters": [ "subClusters": [
@ -96,17 +94,17 @@ func setup(t *testing.T) *api.RestApi {
} }
f.Close() f.Close()
db, err = sqlx.Open("sqlite3", fmt.Sprintf("%s?_foreign_keys=on", dbfilepath)) db, err := sqlx.Open("sqlite3", fmt.Sprintf("%s?_foreign_keys=on", dbfilepath))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
db.SetMaxOpenConns(1) db.SetMaxOpenConns(1)
if _, err := db.Exec(JOBS_DB_SCHEMA); err != nil { if _, err := db.Exec(repository.JobsDBSchema); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := config.Init(db, false, programConfig.UiDefaults, jobarchive); err != nil { if err := config.Init(db, false, map[string]interface{}{}, jobarchive); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -1,26 +0,0 @@
package test
import (
"fmt"
"os"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
func InitDB() *sqlx.DB {
bp := "./"
ebp := os.Getenv("BASEPATH")
if ebp != "" {
bp = ebp + "test/"
}
db, err := sqlx.Open("sqlite3", bp+"test.db")
if err != nil {
fmt.Println(err)
}
return db
}