mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 13:29:05 +01:00
cleanup and comments
This commit is contained in:
parent
f69c3945d4
commit
7be38277a9
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -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
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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
128
routes.go
@ -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{}{}
|
||||||
|
|
||||||
|
@ -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
208
server.go
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
26
test/db.go
26
test/db.go
@ -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
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user