add cli option for generating a JWT; simplify templates

This commit is contained in:
Lou Knauer 2022-01-10 16:14:54 +01:00
parent b7432fca5f
commit 290e9b89bf
7 changed files with 117 additions and 75 deletions

View File

@ -9,7 +9,6 @@ import (
"net/http"
"os"
"path/filepath"
"time"
"github.com/ClusterCockpit/cc-jobarchive/config"
"github.com/ClusterCockpit/cc-jobarchive/graph"
@ -28,15 +27,18 @@ type RestApi struct {
}
func (api *RestApi) MountRoutes(r *mux.Router) {
r.HandleFunc("/api/jobs/start_job/", api.startJob).Methods(http.MethodPost, http.MethodPut)
r.HandleFunc("/api/jobs/stop_job/", api.stopJob).Methods(http.MethodPost, http.MethodPut)
r.HandleFunc("/api/jobs/stop_job/{id}", api.stopJob).Methods(http.MethodPost, http.MethodPut)
r = r.PathPrefix("/api").Subrouter()
r.StrictSlash(true)
r.HandleFunc("/api/jobs/{id}", api.getJob).Methods(http.MethodGet)
r.HandleFunc("/api/jobs/tag_job/{id}", api.tagJob).Methods(http.MethodPost, http.MethodPatch)
r.HandleFunc("/jobs/start_job/", api.startJob).Methods(http.MethodPost, http.MethodPut)
r.HandleFunc("/jobs/stop_job/", api.stopJob).Methods(http.MethodPost, http.MethodPut)
r.HandleFunc("/jobs/stop_job/{id}", api.stopJob).Methods(http.MethodPost, http.MethodPut)
r.HandleFunc("/api/machine_state/{cluster}/{host}", api.getMachineState).Methods(http.MethodGet)
r.HandleFunc("/api/machine_state/{cluster}/{host}", api.putMachineState).Methods(http.MethodPut, http.MethodPost)
r.HandleFunc("/jobs/{id}", api.getJob).Methods(http.MethodGet)
r.HandleFunc("/jobs/tag_job/{id}", api.tagJob).Methods(http.MethodPost, http.MethodPatch)
r.HandleFunc("/machine_state/{cluster}/{host}", api.getMachineState).Methods(http.MethodGet)
r.HandleFunc("/machine_state/{cluster}/{host}", api.putMachineState).Methods(http.MethodPut, http.MethodPost)
}
type StartJobApiRespone struct {
@ -158,17 +160,18 @@ func (api *RestApi) startJob(rw http.ResponseWriter, r *http.Request) {
return
}
job := schema.Job{
BaseJob: req.BaseJob,
StartTime: time.Unix(req.StartTime, 0),
}
job.RawResources, err = json.Marshal(req.Resources)
req.RawResources, err = json.Marshal(req.Resources)
if err != nil {
log.Fatal(err)
}
res, err := api.DB.NamedExec(schema.JobInsertStmt, job)
res, err := api.DB.NamedExec(`INSERT INTO job (
job_id, user, project, cluster, partition, array_job_id, num_nodes, num_hwthreads, num_acc,
exclusive, monitoring_status, smt, job_state, start_time, duration, resources, meta_data
) VALUES (
:job_id, :user, :project, :cluster, :partition, :array_job_id, :num_nodes, :num_hwthreads, :num_acc,
:exclusive, :monitoring_status, :smt, :job_state, :start_time, :duration, :resources, :meta_data
);`, req)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return

View File

@ -23,12 +23,13 @@ import (
)
type User struct {
Username string
Password string
Name string
IsAdmin bool
ViaLdap bool
Email string
Username string
Password string
Name string
IsAdmin bool
IsAPIUser bool
ViaLdap bool
Email string
}
type ContextKey string
@ -110,6 +111,9 @@ func AddUserToDB(db *sqlx.DB, arg string) error {
if parts[1] == "admin" {
roles = "[\"ROLE_ADMIN\"]"
}
if parts[1] == "api" {
roles = "[\"ROLE_API\"]"
}
_, err = sq.Insert("user").Columns("username", "password", "roles").Values(parts[0], string(password), roles).RunWith(db).Exec()
if err != nil {
@ -124,7 +128,7 @@ func DelUserFromDB(db *sqlx.DB, username string) error {
return err
}
func fetchUserFromDB(db *sqlx.DB, username string) (*User, error) {
func FetchUserFromDB(db *sqlx.DB, username string) (*User, error) {
user := &User{Username: username}
var hashedPassword, name, rawRoles, email sql.NullString
if err := sq.Select("password", "ldap", "name", "roles", "email").From("user").
@ -141,8 +145,11 @@ func fetchUserFromDB(db *sqlx.DB, username string) (*User, error) {
json.Unmarshal([]byte(rawRoles.String), &roles)
}
for _, role := range roles {
if role == "ROLE_ADMIN" {
switch role {
case "ROLE_ADMIN":
user.IsAdmin = true
case "ROLE_API":
user.IsAPIUser = true
}
}
@ -154,7 +161,7 @@ func fetchUserFromDB(db *sqlx.DB, username string) (*User, error) {
func Login(db *sqlx.DB) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
username, password := r.FormValue("username"), r.FormValue("password")
user, err := fetchUserFromDB(db, username)
user, err := FetchUserFromDB(db, username)
if err == nil && user.ViaLdap && ldapAuthEnabled {
err = loginViaLdap(user, password)
} else if err == nil && !user.ViaLdap && user.Password != "" {
@ -168,7 +175,7 @@ func Login(db *sqlx.DB) http.Handler {
if err != nil {
log.Printf("login failed: %s\n", err.Error())
rw.WriteHeader(http.StatusUnauthorized)
templates.Render(rw, r, "login", &templates.Page{
templates.Render(rw, r, "login.html", &templates.Page{
Title: "Login failed",
Login: &templates.LoginPage{
Error: "Username or password incorrect",
@ -231,9 +238,11 @@ func authViaToken(r *http.Request) (*User, error) {
claims := token.Claims.(jwt.MapClaims)
sub, _ := claims["sub"].(string)
isAdmin, _ := claims["is_admin"].(bool)
isAPIUser, _ := claims["is_api"].(bool)
return &User{
Username: sub,
IsAdmin: isAdmin,
Username: sub,
IsAdmin: isAdmin,
IsAPIUser: isAPIUser,
}, nil
}
@ -264,7 +273,7 @@ func Auth(next http.Handler) http.Handler {
log.Printf("authentication failed: no session or jwt found\n")
rw.WriteHeader(http.StatusUnauthorized)
templates.Render(rw, r, "login", &templates.Page{
templates.Render(rw, r, "login.html", &templates.Page{
Title: "Authentication failed",
Login: &templates.LoginPage{
Error: "No valid session or JWT provided",
@ -290,6 +299,7 @@ func ProvideJWT(user *User) (string, error) {
tok := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
"sub": user.Username,
"is_admin": user.IsAdmin,
"is_api": user.IsAPIUser,
})
return tok.SignedString(JwtPrivateKey)
@ -320,7 +330,7 @@ func Logout(rw http.ResponseWriter, r *http.Request) {
}
}
templates.Render(rw, r, "login", &templates.Page{
templates.Render(rw, r, "login.html", &templates.Page{
Title: "Logout successful",
Login: &templates.LoginPage{
Info: "Logout successful",

6
go.sum
View File

@ -12,6 +12,7 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -68,14 +69,17 @@ github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -84,6 +88,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns=
@ -112,6 +117,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -106,13 +106,14 @@ var programConfig ProgramConfig = ProgramConfig{
func main() {
var flagReinitDB, flagStopImmediately, flagSyncLDAP bool
var flagConfigFile string
var flagNewUser, flagDelUser 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(&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.StringVar(&flagConfigFile, "config", "", "Location of the config file for this server (overwrites the defaults)")
flag.StringVar(&flagNewUser, "add-user", "", "Add a new user. Argument format: `<username>:[admin]:<password>`")
flag.StringVar(&flagNewUser, "add-user", "", "Add a new user. Argument format: `<username>:[admin|api]:<password>`")
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.Parse()
if flagConfigFile != "" {
@ -156,6 +157,24 @@ func main() {
if flagSyncLDAP {
auth.SyncWithLDAP(db)
}
if flagGenJWT != "" {
user, err := auth.FetchUserFromDB(db, flagGenJWT)
if err != nil {
log.Fatal(err)
}
if !user.IsAPIUser {
log.Println("warning: that user does not have the API role")
}
jwt, err := auth.ProvideJWT(user)
if err != nil {
log.Fatal(err)
}
fmt.Printf("JWT for '%s': %s\n", user.Username, jwt)
}
} else if flagNewUser != "" || flagDelUser != "" {
log.Fatalln("arguments --add-user and --del-user can only be used if authentication is enabled")
}
@ -182,6 +201,18 @@ func main() {
resolver := &graph.Resolver{DB: db}
graphQLEndpoint := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: resolver}))
// graphQLEndpoint.SetRecoverFunc(func(ctx context.Context, err interface{}) error {
// switch e := err.(type) {
// case string:
// return fmt.Errorf("panic: %s", e)
// case error:
// return fmt.Errorf("panic caused by: %w", e)
// }
// return errors.New("internal server error (panic)")
// })
graphQLPlayground := playground.Handler("GraphQL playground", "/query")
api := &api.RestApi{
DB: db,
@ -191,7 +222,7 @@ func main() {
}
handleGetLogin := func(rw http.ResponseWriter, r *http.Request) {
templates.Render(rw, r, "login", &templates.Page{
templates.Render(rw, r, "login.html", &templates.Page{
Title: "Login",
Login: &templates.LoginPage{},
})
@ -199,7 +230,7 @@ func main() {
r := mux.NewRouter()
r.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
templates.Render(rw, r, "404", &templates.Page{
templates.Render(rw, r, "404.html", &templates.Page{
Title: "Not found",
})
})
@ -215,8 +246,6 @@ func main() {
}
secured.Handle("/query", graphQLEndpoint)
secured.HandleFunc("/config.json", config.ServeConfig).Methods(http.MethodGet)
secured.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
conf, err := config.GetUIConfig(r)
if err != nil {
@ -235,7 +264,7 @@ func main() {
infos["admin"] = user.IsAdmin
}
templates.Render(rw, r, "home", &templates.Page{
templates.Render(rw, r, "home.html", &templates.Page{
Title: "ClusterCockpit",
Config: conf,
Infos: infos,
@ -297,7 +326,7 @@ func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
return
}
templates.Render(rw, r, "monitoring/jobs/", &templates.Page{
templates.Render(rw, r, "monitoring/jobs.html", &templates.Page{
Title: "Jobs - ClusterCockpit",
Config: conf,
FilterPresets: buildFilterPresets(r.URL.Query()),
@ -318,7 +347,7 @@ func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
return
}
templates.Render(rw, r, "monitoring/job/", &templates.Page{
templates.Render(rw, r, "monitoring/job.html", &templates.Page{
Title: fmt.Sprintf("Job %d - ClusterCockpit", job.JobID),
Config: conf,
Infos: map[string]interface{}{
@ -336,7 +365,7 @@ func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
return
}
templates.Render(rw, r, "monitoring/users/", &templates.Page{
templates.Render(rw, r, "monitoring/users.html", &templates.Page{
Title: "Users - ClusterCockpit",
Config: conf,
})
@ -353,7 +382,7 @@ func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
// TODO: One could check if the user exists, but that would be unhelpfull if authentication
// is disabled or the user does not exist but has started jobs.
templates.Render(rw, r, "monitoring/user/", &templates.Page{
templates.Render(rw, r, "monitoring/user.html", &templates.Page{
Title: fmt.Sprintf("User %s - ClusterCockpit", id),
Config: conf,
Infos: map[string]interface{}{"username": id},
@ -374,7 +403,7 @@ func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
filterPresets["clusterId"] = query.Get("cluster")
}
templates.Render(rw, r, "monitoring/analysis/", &templates.Page{
templates.Render(rw, r, "monitoring/analysis.html", &templates.Page{
Title: "Analysis View - ClusterCockpit",
Config: conf,
FilterPresets: filterPresets,
@ -394,7 +423,7 @@ func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
filterPresets["clusterId"] = query.Get("cluster")
}
templates.Render(rw, r, "monitoring/systems/", &templates.Page{
templates.Render(rw, r, "monitoring/systems.html", &templates.Page{
Title: "System View - ClusterCockpit",
Config: conf,
FilterPresets: filterPresets,
@ -409,7 +438,7 @@ func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
}
vars := mux.Vars(r)
templates.Render(rw, r, "monitoring/node/", &templates.Page{
templates.Render(rw, r, "monitoring/node.html", &templates.Page{
Title: fmt.Sprintf("Node %s - ClusterCockpit", vars["nodeId"]),
Config: conf,
Infos: map[string]interface{}{

View File

@ -8,13 +8,7 @@
{{define "javascript"}}
<script>
const filterPresets = {{ .FilterPresets }};
const clusterCockpitConfigPromise = Promise.resolve({
plot_general_colorscheme: {{ .Config.plot_general_colorscheme }},
plot_general_lineWidth: {{ .Config.plot_general_lineWidth }},
plot_general_colorBackground: {{ .Config.plot_general_colorBackground }},
plot_list_selectedMetrics: {{ .Config.plot_list_selectedMetrics }},
plot_list_jobsPerPage: {{ .Config.plot_list_jobsPerPage }}
});
const clusterCockpitConfig = {{ .Config }};
</script>
<script src='/build/jobs.js'></script>
{{end}}

View File

@ -7,16 +7,9 @@
{{end}}
{{define "javascript"}}
<script>
const userInfos = {
userId: "{{ .Infos.userId }}"
};
const clusterCockpitConfigPromise = Promise.resolve({
plot_general_colorscheme: {{ .Config.plot_general_colorscheme }},
plot_general_lineWidth: {{ .Config.plot_general_lineWidth }},
plot_general_colorBackground: {{ .Config.plot_general_colorBackground }},
plot_list_selectedMetrics: {{ .Config.plot_list_selectedMetrics }},
plot_list_jobsPerPage: {{ .Config.plot_list_jobsPerPage }}
});
const userInfos = {{ .Infos }};
const filterPresets = {{ .FilterPresets }};
const clusterCockpitConfig = {{ .Config }};
</script>
<script src='/build/user.js'></script>
{{end}}

View File

@ -6,7 +6,9 @@ import (
"net/http"
)
var templates map[string]*template.Template
var templatesDir string
var debugMode bool = true
var templates map[string]*template.Template = map[string]*template.Template{}
type Page struct {
Title string
@ -22,27 +24,32 @@ type LoginPage struct {
}
func init() {
base := template.Must(template.ParseFiles("./templates/base.html"))
templates = map[string]*template.Template{
"home": template.Must(template.Must(base.Clone()).ParseFiles("./templates/home.html")),
"404": template.Must(template.Must(base.Clone()).ParseFiles("./templates/404.html")),
"login": template.Must(template.Must(base.Clone()).ParseFiles("./templates/login.html")),
"monitoring/jobs/": template.Must(template.Must(base.Clone()).ParseFiles("./templates/monitoring/jobs.html")),
"monitoring/job/": template.Must(template.Must(base.Clone()).ParseFiles("./templates/monitoring/job.html")),
"monitoring/users/": template.Must(template.Must(base.Clone()).ParseFiles("./templates/monitoring/users.html")),
"monitoring/user/": template.Must(template.Must(base.Clone()).ParseFiles("./templates/monitoring/user.html")),
"monitoring/analysis/": template.Must(template.Must(base.Clone()).ParseFiles("./templates/monitoring/analysis.html")),
"monitoring/systems/": template.Must(template.Must(base.Clone()).ParseFiles("./templates/monitoring/systems.html")),
"monitoring/node/": template.Must(template.Must(base.Clone()).ParseFiles("./templates/monitoring/node.html")),
templatesDir = "./templates/"
base := template.Must(template.ParseFiles(templatesDir + "base.html"))
files := []string{
"home.html", "404.html", "login.html",
"monitoring/jobs.html", "monitoring/job.html",
"monitoring/users.html", "monitoring/user.html",
"monitoring/analysis.html",
"monitoring/systems.html",
"monitoring/node.html",
}
for _, file := range files {
templates[file] = template.Must(template.Must(base.Clone()).ParseFiles(templatesDir + file))
}
}
func Render(rw http.ResponseWriter, r *http.Request, name string, page *Page) {
t, ok := templates[name]
func Render(rw http.ResponseWriter, r *http.Request, file string, page *Page) {
t, ok := templates[file]
if !ok {
panic("templates must be predefinied!")
}
if debugMode {
t = template.Must(template.ParseFiles(templatesDir+"base.html", templatesDir+file))
}
if err := t.Execute(rw, page); err != nil {
log.Printf("template error: %s\n", err.Error())
}