From 5a8dcfac1ea86335a993325899d4f4637887f03d Mon Sep 17 00:00:00 2001 From: evanebb <78433178+evanebb@users.noreply.github.com> Date: Sat, 6 Jan 2024 22:15:41 +0100 Subject: [PATCH 1/8] Add user interface built with Bootstrap and Go HTML templates --- api/auth/auth.go | 33 ++- resources/resources.go | 9 + resources/static/css/style.css | 7 + resources/templates/404.gohtml | 9 + resources/templates/base.gohtml | 56 +++++ resources/templates/error.gohtml | 5 + resources/templates/home.gohtml | 6 + resources/templates/profiles/create.gohtml | 28 +++ resources/templates/profiles/edit.gohtml | 35 +++ resources/templates/profiles/overview.gohtml | 25 +++ resources/templates/profiles/show.gohtml | 37 ++++ resources/templates/systems/create.gohtml | 28 +++ resources/templates/systems/edit.gohtml | 44 ++++ resources/templates/systems/overview.gohtml | 25 +++ resources/templates/systems/show.gohtml | 44 ++++ .../{ => api_handlers}/api_user_handlers.go | 9 +- server/handlers/{ => api_handlers}/errors.go | 2 +- .../handlers/{ => api_handlers}/handlers.go | 7 +- .../{ => api_handlers}/profile_handlers.go | 11 +- .../{ => api_handlers}/pxe_config_handlers.go | 2 +- .../{ => api_handlers}/system_handlers.go | 11 +- server/handlers/common.go | 4 +- server/handlers/middleware.go | 19 ++ server/handlers/ui_handlers/handlers.go | 13 ++ .../handlers/ui_handlers/profile_handlers.go | 165 ++++++++++++++ .../handlers/ui_handlers/system_handlers.go | 202 ++++++++++++++++++ server/handlers/ui_handlers/template.go | 50 +++++ server/routes.go | 99 ++++++--- 28 files changed, 930 insertions(+), 55 deletions(-) create mode 100644 resources/resources.go create mode 100644 resources/static/css/style.css create mode 100644 resources/templates/404.gohtml create mode 100644 resources/templates/base.gohtml create mode 100644 resources/templates/error.gohtml create mode 100644 resources/templates/home.gohtml create mode 100644 resources/templates/profiles/create.gohtml create mode 100644 resources/templates/profiles/edit.gohtml create mode 100644 resources/templates/profiles/overview.gohtml create mode 100644 resources/templates/profiles/show.gohtml create mode 100644 resources/templates/systems/create.gohtml create mode 100644 resources/templates/systems/edit.gohtml create mode 100644 resources/templates/systems/overview.gohtml create mode 100644 resources/templates/systems/show.gohtml rename server/handlers/{ => api_handlers}/api_user_handlers.go (94%) rename server/handlers/{ => api_handlers}/errors.go (93%) rename server/handlers/{ => api_handlers}/handlers.go (86%) rename server/handlers/{ => api_handlers}/profile_handlers.go (95%) rename server/handlers/{ => api_handlers}/pxe_config_handlers.go (98%) rename server/handlers/{ => api_handlers}/system_handlers.go (96%) create mode 100644 server/handlers/middleware.go create mode 100644 server/handlers/ui_handlers/handlers.go create mode 100644 server/handlers/ui_handlers/profile_handlers.go create mode 100644 server/handlers/ui_handlers/system_handlers.go create mode 100644 server/handlers/ui_handlers/template.go diff --git a/api/auth/auth.go b/api/auth/auth.go index dbb64a3..d0a0648 100644 --- a/api/auth/auth.go +++ b/api/auth/auth.go @@ -1,28 +1,43 @@ package auth import ( + "fmt" "github.com/evanebb/gobble/api/response" "net/http" ) -func BasicAuth(db ApiUserRepository) func(next http.Handler) http.Handler { +// ApiBasicAuth will check the basic auth credentials sent in the request against the known users, +// and return a JSON response if authentication has failed. +func ApiBasicAuth(db ApiUserRepository) func(next http.Handler) http.Handler { + return basicAuth(db, sendBasicAuthFailedResponse) +} + +// BrowserBasicAuth will check the basic auth credentials sent in the request against the known users, +// and (re)-request basic auth credentials if authentication has failed. +func BrowserBasicAuth(db ApiUserRepository) func(next http.Handler) http.Handler { + return basicAuth(db, requestBasicAuth) +} + +// basicAuth will check the basic auth credentials sent in the request against the known users, +// and execute the passed callback if authentication has failed. +func basicAuth(db ApiUserRepository, authFailureCallback func(w http.ResponseWriter)) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() if !ok { - basicAuthFailed(w) + authFailureCallback(w) return } apiUser, err := db.GetApiUserByName(user) if err != nil { - basicAuthFailed(w) + authFailureCallback(w) return } err = apiUser.CheckPassword(pass) if err != nil { - basicAuthFailed(w) + authFailureCallback(w) return } @@ -31,9 +46,17 @@ func BasicAuth(db ApiUserRepository) func(next http.Handler) http.Handler { } } -func basicAuthFailed(w http.ResponseWriter) { +// sendBasicAuthFailedResponse will write a JSON response indicating authentication failure to the passed http.ResponseWriter variable. +func sendBasicAuthFailedResponse(w http.ResponseWriter) { err := response.Error(w, http.StatusUnauthorized, "authentication failed") if err != nil { http.Error(w, "internal server error", http.StatusInternalServerError) } } + +// sendBasicAuthFailedResponse will request basic authentication by sending the 'WWW-Authenticate' header with a 401 status code. +func requestBasicAuth(w http.ResponseWriter) { + w.Header().Set("WWW-Authenticate", `Basic realm="gobble"`) + w.WriteHeader(401) + _, _ = fmt.Fprint(w, "Unauthorised") +} diff --git a/resources/resources.go b/resources/resources.go new file mode 100644 index 0000000..d284591 --- /dev/null +++ b/resources/resources.go @@ -0,0 +1,9 @@ +package resources + +import "embed" + +//go:embed templates +var Templates embed.FS + +//go:embed static +var Static embed.FS diff --git a/resources/static/css/style.css b/resources/static/css/style.css new file mode 100644 index 0000000..4e29e78 --- /dev/null +++ b/resources/static/css/style.css @@ -0,0 +1,7 @@ +.alternate-bg-color .row:nth-of-type(odd) { + background-color: var(--bs-card-cap-bg); +} + +.table-fixed-width { + table-layout: fixed; +} diff --git a/resources/templates/404.gohtml b/resources/templates/404.gohtml new file mode 100644 index 0000000..66c5d5f --- /dev/null +++ b/resources/templates/404.gohtml @@ -0,0 +1,9 @@ +{{ define "content" }} +
+
+

404

+

Oops! There is nothing here.

+ Go to homepage +
+
+{{ end }} \ No newline at end of file diff --git a/resources/templates/base.gohtml b/resources/templates/base.gohtml new file mode 100644 index 0000000..ec873ef --- /dev/null +++ b/resources/templates/base.gohtml @@ -0,0 +1,56 @@ +{{ define "base" }} + + + Gobble - {{ .Title }} + + + + + + {{ if not .DisableNavbar }} + + {{ end }} +
+ {{ template "content" .Data}} +
+ + + +{{ end }} \ No newline at end of file diff --git a/resources/templates/error.gohtml b/resources/templates/error.gohtml new file mode 100644 index 0000000..070ba53 --- /dev/null +++ b/resources/templates/error.gohtml @@ -0,0 +1,5 @@ +{{ define "content" }} +
+

Error occurred: {{ . }}

+
+{{ end }} \ No newline at end of file diff --git a/resources/templates/home.gohtml b/resources/templates/home.gohtml new file mode 100644 index 0000000..9badaed --- /dev/null +++ b/resources/templates/home.gohtml @@ -0,0 +1,6 @@ +{{ define "content" }} +
+

Home

+

Welcome to Gobble!

+
+{{ end }} \ No newline at end of file diff --git a/resources/templates/profiles/create.gohtml b/resources/templates/profiles/create.gohtml new file mode 100644 index 0000000..13ddb4c --- /dev/null +++ b/resources/templates/profiles/create.gohtml @@ -0,0 +1,28 @@ +{{ define "content" }} +
+

Create profile

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+{{ end }} \ No newline at end of file diff --git a/resources/templates/profiles/edit.gohtml b/resources/templates/profiles/edit.gohtml new file mode 100644 index 0000000..353f13f --- /dev/null +++ b/resources/templates/profiles/edit.gohtml @@ -0,0 +1,35 @@ +{{ define "content" }} +
+

Edit profile

+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + Cancel +
+
+{{ end }} \ No newline at end of file diff --git a/resources/templates/profiles/overview.gohtml b/resources/templates/profiles/overview.gohtml new file mode 100644 index 0000000..902ab2d --- /dev/null +++ b/resources/templates/profiles/overview.gohtml @@ -0,0 +1,25 @@ +{{ define "content" }} +
+

Profiles

+
+ + + + + + + + + + {{range $val := .}} + + + + + + {{end}} + +
IDNameDescription
{{$val.Id}}{{$val.Name}}{{$val.Description}}
+
+
+{{ end }} \ No newline at end of file diff --git a/resources/templates/profiles/show.gohtml b/resources/templates/profiles/show.gohtml new file mode 100644 index 0000000..fd4a9fe --- /dev/null +++ b/resources/templates/profiles/show.gohtml @@ -0,0 +1,37 @@ +{{ define "content" }} +
+

Profile

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ Edit + + +
+
+{{ end }} \ No newline at end of file diff --git a/resources/templates/systems/create.gohtml b/resources/templates/systems/create.gohtml new file mode 100644 index 0000000..f8116b7 --- /dev/null +++ b/resources/templates/systems/create.gohtml @@ -0,0 +1,28 @@ +{{ define "content" }} +
+

Create system

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+{{ end }} \ No newline at end of file diff --git a/resources/templates/systems/edit.gohtml b/resources/templates/systems/edit.gohtml new file mode 100644 index 0000000..ef17f50 --- /dev/null +++ b/resources/templates/systems/edit.gohtml @@ -0,0 +1,44 @@ +{{ define "content" }} +
+

Edit system

+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + Cancel +
+
+{{ end }} \ No newline at end of file diff --git a/resources/templates/systems/overview.gohtml b/resources/templates/systems/overview.gohtml new file mode 100644 index 0000000..f45efed --- /dev/null +++ b/resources/templates/systems/overview.gohtml @@ -0,0 +1,25 @@ +{{ define "content" }} +
+

Systems

+
+ + + + + + + + + + {{range $val := .}} + + + + + + {{end}} + +
IDNameDescription
{{$val.Id}}{{$val.Name}}{{$val.Description}}
+
+
+{{ end }} \ No newline at end of file diff --git a/resources/templates/systems/show.gohtml b/resources/templates/systems/show.gohtml new file mode 100644 index 0000000..6872d8f --- /dev/null +++ b/resources/templates/systems/show.gohtml @@ -0,0 +1,44 @@ +{{ define "content" }} +
+

System

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ Edit + + +
+
+{{ end }} \ No newline at end of file diff --git a/server/handlers/api_user_handlers.go b/server/handlers/api_handlers/api_user_handlers.go similarity index 94% rename from server/handlers/api_user_handlers.go rename to server/handlers/api_handlers/api_user_handlers.go index 7ffb245..95a0e04 100644 --- a/server/handlers/api_user_handlers.go +++ b/server/handlers/api_handlers/api_user_handlers.go @@ -1,4 +1,4 @@ -package handlers +package api_handlers import ( "encoding/json" @@ -6,6 +6,7 @@ import ( "github.com/evanebb/gobble/api/auth" "github.com/evanebb/gobble/api/response" "github.com/evanebb/gobble/repository" + "github.com/evanebb/gobble/server/handlers" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" "net/http" @@ -63,7 +64,7 @@ func (h ApiUserHandlerGroup) GetUsers(w http.ResponseWriter, r *http.Request) er } func (h ApiUserHandlerGroup) GetUser(w http.ResponseWriter, r *http.Request) error { - userID, err := getUUIDFromRequest(r) + userID, err := handlers.GetUUIDFromRequest(r) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } @@ -108,7 +109,7 @@ func (h ApiUserHandlerGroup) CreateUser(w http.ResponseWriter, r *http.Request) func (h ApiUserHandlerGroup) PutUser(w http.ResponseWriter, r *http.Request) error { var req userRequest - userID, err := getUUIDFromRequest(r) + userID, err := handlers.GetUUIDFromRequest(r) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } @@ -135,7 +136,7 @@ func (h ApiUserHandlerGroup) PutUser(w http.ResponseWriter, r *http.Request) err } func (h ApiUserHandlerGroup) DeleteUser(w http.ResponseWriter, r *http.Request) error { - userID, err := getUUIDFromRequest(r) + userID, err := handlers.GetUUIDFromRequest(r) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } diff --git a/server/handlers/errors.go b/server/handlers/api_handlers/errors.go similarity index 93% rename from server/handlers/errors.go rename to server/handlers/api_handlers/errors.go index 1eab00a..38ae426 100644 --- a/server/handlers/errors.go +++ b/server/handlers/api_handlers/errors.go @@ -1,4 +1,4 @@ -package handlers +package api_handlers type HTTPError struct { err error diff --git a/server/handlers/handlers.go b/server/handlers/api_handlers/handlers.go similarity index 86% rename from server/handlers/handlers.go rename to server/handlers/api_handlers/handlers.go index e321870..84a29b3 100644 --- a/server/handlers/handlers.go +++ b/server/handlers/api_handlers/handlers.go @@ -1,4 +1,4 @@ -package handlers +package api_handlers import ( "errors" @@ -49,8 +49,3 @@ func ErrorHandler(h ErrorHandlerFunc) http.HandlerFunc { func UnknownEndpointHandler(w http.ResponseWriter, r *http.Request) error { return response.Error(w, http.StatusNotFound, "unknown endpoint, please refer to the documentation for available endpoints") } - -func IndexHandler(w http.ResponseWriter, r *http.Request) error { - html := "

Welcome to gobble!

Refer to the documentation for the available API endpoints.

" - return response.HTML(w, http.StatusOK, html) -} diff --git a/server/handlers/profile_handlers.go b/server/handlers/api_handlers/profile_handlers.go similarity index 95% rename from server/handlers/profile_handlers.go rename to server/handlers/api_handlers/profile_handlers.go index 21b73f4..63b34c5 100644 --- a/server/handlers/profile_handlers.go +++ b/server/handlers/api_handlers/profile_handlers.go @@ -1,4 +1,4 @@ -package handlers +package api_handlers import ( "encoding/json" @@ -7,6 +7,7 @@ import ( "github.com/evanebb/gobble/kernelparameters" "github.com/evanebb/gobble/profile" "github.com/evanebb/gobble/repository" + "github.com/evanebb/gobble/server/handlers" "github.com/google/uuid" "net/http" ) @@ -74,7 +75,7 @@ func (h ProfileHandlerGroup) GetProfiles(w http.ResponseWriter, r *http.Request) } func (h ProfileHandlerGroup) GetProfile(w http.ResponseWriter, r *http.Request) error { - profileId, err := getUUIDFromRequest(r) + profileId, err := handlers.GetUUIDFromRequest(r) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } @@ -122,7 +123,7 @@ func (h ProfileHandlerGroup) CreateProfile(w http.ResponseWriter, r *http.Reques func (h ProfileHandlerGroup) PutProfile(w http.ResponseWriter, r *http.Request) error { var req profileRequest - profileId, err := getUUIDFromRequest(r) + profileId, err := handlers.GetUUIDFromRequest(r) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } @@ -153,7 +154,7 @@ func (h ProfileHandlerGroup) PutProfile(w http.ResponseWriter, r *http.Request) } func (h ProfileHandlerGroup) PatchProfile(w http.ResponseWriter, r *http.Request) error { - profileId, err := getUUIDFromRequest(r) + profileId, err := handlers.GetUUIDFromRequest(r) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } @@ -202,7 +203,7 @@ func (h ProfileHandlerGroup) PatchProfile(w http.ResponseWriter, r *http.Request } func (h ProfileHandlerGroup) DeleteProfile(w http.ResponseWriter, r *http.Request) error { - profileId, err := getUUIDFromRequest(r) + profileId, err := handlers.GetUUIDFromRequest(r) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } diff --git a/server/handlers/pxe_config_handlers.go b/server/handlers/api_handlers/pxe_config_handlers.go similarity index 98% rename from server/handlers/pxe_config_handlers.go rename to server/handlers/api_handlers/pxe_config_handlers.go index a98f249..e978ca1 100644 --- a/server/handlers/pxe_config_handlers.go +++ b/server/handlers/api_handlers/pxe_config_handlers.go @@ -1,4 +1,4 @@ -package handlers +package api_handlers import ( "errors" diff --git a/server/handlers/system_handlers.go b/server/handlers/api_handlers/system_handlers.go similarity index 96% rename from server/handlers/system_handlers.go rename to server/handlers/api_handlers/system_handlers.go index b8901a6..3cdf34d 100644 --- a/server/handlers/system_handlers.go +++ b/server/handlers/api_handlers/system_handlers.go @@ -1,4 +1,4 @@ -package handlers +package api_handlers import ( "encoding/json" @@ -6,6 +6,7 @@ import ( "github.com/evanebb/gobble/api/response" "github.com/evanebb/gobble/kernelparameters" "github.com/evanebb/gobble/repository" + "github.com/evanebb/gobble/server/handlers" "github.com/evanebb/gobble/system" "github.com/google/uuid" "net" @@ -75,7 +76,7 @@ func (h SystemHandlerGroup) GetSystems(w http.ResponseWriter, r *http.Request) e } func (h SystemHandlerGroup) GetSystem(w http.ResponseWriter, r *http.Request) error { - systemId, err := getUUIDFromRequest(r) + systemId, err := handlers.GetUUIDFromRequest(r) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } @@ -128,7 +129,7 @@ func (h SystemHandlerGroup) CreateSystem(w http.ResponseWriter, r *http.Request) func (h SystemHandlerGroup) PutSystem(w http.ResponseWriter, r *http.Request) error { var req systemRequest - systemId, err := getUUIDFromRequest(r) + systemId, err := handlers.GetUUIDFromRequest(r) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } @@ -164,7 +165,7 @@ func (h SystemHandlerGroup) PutSystem(w http.ResponseWriter, r *http.Request) er } func (h SystemHandlerGroup) PatchSystem(w http.ResponseWriter, r *http.Request) error { - systemId, err := getUUIDFromRequest(r) + systemId, err := handlers.GetUUIDFromRequest(r) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } @@ -218,7 +219,7 @@ func (h SystemHandlerGroup) PatchSystem(w http.ResponseWriter, r *http.Request) } func (h SystemHandlerGroup) DeleteSystem(w http.ResponseWriter, r *http.Request) error { - systemId, err := getUUIDFromRequest(r) + systemId, err := handlers.GetUUIDFromRequest(r) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } diff --git a/server/handlers/common.go b/server/handlers/common.go index 4a297a2..ed58059 100644 --- a/server/handlers/common.go +++ b/server/handlers/common.go @@ -7,8 +7,8 @@ import ( "net/http" ) -// getUUIDFromRequest gets and parses the UUID from the request. If it's not a valid UUID, an error is returned. -func getUUIDFromRequest(r *http.Request) (uuid.UUID, error) { +// GetUUIDFromRequest gets and parses the UUID from the request. If it's not a valid UUID, an error is returned. +func GetUUIDFromRequest(r *http.Request) (uuid.UUID, error) { uuidString := chi.URLParam(r, "uuid") UUID, err := uuid.Parse(uuidString) if err != nil { diff --git a/server/handlers/middleware.go b/server/handlers/middleware.go new file mode 100644 index 0000000..822b27d --- /dev/null +++ b/server/handlers/middleware.go @@ -0,0 +1,19 @@ +package handlers + +import "net/http" + +// MethodOverride will check for a hidden "_method" input in the POST form, +// so that PUT, PATCH and DELETE requests can be supported +func MethodOverride(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + method := r.PostFormValue("_method") + + if method == "PUT" || method == "PATCH" || method == "DELETE" { + r.Method = method + } + } + + next.ServeHTTP(w, r) + }) +} diff --git a/server/handlers/ui_handlers/handlers.go b/server/handlers/ui_handlers/handlers.go new file mode 100644 index 0000000..0e1b6ea --- /dev/null +++ b/server/handlers/ui_handlers/handlers.go @@ -0,0 +1,13 @@ +package ui_handlers + +import "net/http" + +func PageNotFound(w http.ResponseWriter, r *http.Request) { + d := templateData{Title: "Page not found", DisableNavbar: true} + renderTemplate(w, "404", d) +} + +func HomePage(w http.ResponseWriter, r *http.Request) { + d := templateData{Title: "Home"} + renderTemplate(w, "home", d) +} diff --git a/server/handlers/ui_handlers/profile_handlers.go b/server/handlers/ui_handlers/profile_handlers.go new file mode 100644 index 0000000..10555a5 --- /dev/null +++ b/server/handlers/ui_handlers/profile_handlers.go @@ -0,0 +1,165 @@ +package ui_handlers + +import ( + "errors" + "github.com/evanebb/gobble/kernelparameters" + "github.com/evanebb/gobble/profile" + "github.com/evanebb/gobble/server/handlers" + "github.com/google/uuid" + "net/http" + "strings" +) + +func parseProfileFromPostForm(r *http.Request) (profile.Profile, error) { + var p profile.Profile + + err := r.ParseForm() + if err != nil { + return p, err + } + + requiredKeys := []string{"name", "description", "kernel", "initrd", "kernelParameters"} + for _, v := range requiredKeys { + if !r.PostForm.Has(v) { + return p, errors.New("missing value " + v + " in POST form") + } + } + + kp, err := kernelparameters.New(strings.Split(r.PostFormValue("kernelParameters"), " ")) + if err != nil { + return p, err + } + + return profile.New( + // the UUID needs to be set properly afterward by the caller, depending on whether we are creating a new one or updating an existing one + uuid.Nil, + r.PostFormValue("name"), + r.PostFormValue("description"), + r.PostFormValue("kernel"), + r.PostFormValue("initrd"), + kp, + ) +} + +type UiProfileHandlerGroup struct { + profileRepo profile.Repository +} + +func NewUiProfileHandlerGroup(pr profile.Repository) UiProfileHandlerGroup { + return UiProfileHandlerGroup{pr} +} + +// Overview will list all profiles. +func (h UiProfileHandlerGroup) Overview(w http.ResponseWriter, r *http.Request) { + profiles, err := h.profileRepo.GetProfiles() + if err != nil { + renderError(w, err) + return + } + + d := templateData{Title: "Profiles", Data: profiles} + renderTemplate(w, "profiles/overview", d) +} + +// Show will show information about a single profile. +func (h UiProfileHandlerGroup) Show(w http.ResponseWriter, r *http.Request) { + profileId, err := handlers.GetUUIDFromRequest(r) + if err != nil { + renderError(w, err) + return + } + + p, err := h.profileRepo.GetProfileById(profileId) + if err != nil { + renderError(w, err) + return + } + + d := templateData{Title: "Profile Information", Data: p} + renderTemplate(w, "profiles/show", d) +} + +// Create shows the page for creating a new profile. +func (h UiProfileHandlerGroup) Create(w http.ResponseWriter, r *http.Request) { + d := templateData{Title: "Create profile"} + renderTemplate(w, "profiles/create", d) +} + +// Store will store a newly created profile. +func (h UiProfileHandlerGroup) Store(w http.ResponseWriter, r *http.Request) { + p, err := parseProfileFromPostForm(r) + if err != nil { + renderError(w, err) + return + } + + p.Id = uuid.New() + + err = h.profileRepo.SetProfile(p) + if err != nil { + renderError(w, err) + return + } + + http.Redirect(w, r, "/ui/profiles/"+p.Id.String(), http.StatusSeeOther) +} + +// Edit shows the page for editing an existing profile. +func (h UiProfileHandlerGroup) Edit(w http.ResponseWriter, r *http.Request) { + profileId, err := handlers.GetUUIDFromRequest(r) + if err != nil { + renderError(w, err) + return + } + + p, err := h.profileRepo.GetProfileById(profileId) + if err != nil { + renderError(w, err) + return + } + + d := templateData{Title: "Edit Profile", Data: p} + renderTemplate(w, "profiles/edit", d) +} + +// Update will update the specified profile. +func (h UiProfileHandlerGroup) Update(w http.ResponseWriter, r *http.Request) { + profileId, err := handlers.GetUUIDFromRequest(r) + if err != nil { + renderError(w, err) + return + } + + p, err := parseProfileFromPostForm(r) + if err != nil { + renderError(w, err) + return + } + + p.Id = profileId + + err = h.profileRepo.SetProfile(p) + if err != nil { + renderError(w, err) + return + } + + http.Redirect(w, r, "/ui/profiles/"+p.Id.String(), http.StatusSeeOther) +} + +// Delete will delete the specified profile. +func (h UiProfileHandlerGroup) Delete(w http.ResponseWriter, r *http.Request) { + profileId, err := handlers.GetUUIDFromRequest(r) + if err != nil { + renderError(w, err) + return + } + + err = h.profileRepo.DeleteProfileById(profileId) + if err != nil { + renderError(w, err) + return + } + + http.Redirect(w, r, "/ui/profiles", http.StatusSeeOther) +} diff --git a/server/handlers/ui_handlers/system_handlers.go b/server/handlers/ui_handlers/system_handlers.go new file mode 100644 index 0000000..eec40af --- /dev/null +++ b/server/handlers/ui_handlers/system_handlers.go @@ -0,0 +1,202 @@ +package ui_handlers + +import ( + "errors" + "github.com/evanebb/gobble/kernelparameters" + "github.com/evanebb/gobble/profile" + "github.com/evanebb/gobble/server/handlers" + "github.com/evanebb/gobble/system" + "github.com/google/uuid" + "net" + "net/http" + "strings" +) + +func parseSystemFromPostForm(r *http.Request) (system.System, error) { + var s system.System + + err := r.ParseForm() + if err != nil { + return s, err + } + + requiredKeys := []string{"name", "description", "profile", "mac", "kernelParameters"} + for _, v := range requiredKeys { + if !r.PostForm.Has(v) { + return s, errors.New("missing value " + v + " in POST form") + } + } + + kp, err := kernelparameters.New(strings.Split(r.PostFormValue("kernelParameters"), " ")) + if err != nil { + return s, err + } + + profileId, err := uuid.Parse(r.PostFormValue("profile")) + if err != nil { + return s, err + } + + macAddress, err := net.ParseMAC(r.PostFormValue("mac")) + if err != nil { + return s, err + } + + return system.New( + // the UUID needs to be set properly afterward by the caller, depending on whether we are creating a new one or updating an existing one + uuid.Nil, + r.PostFormValue("name"), + r.PostFormValue("description"), + profileId, + macAddress, + kp, + ) +} + +type UiSystemHandlerGroup struct { + systemRepo system.Repository + profileRepo profile.Repository +} + +func NewUiSystemHandlerGroup(sr system.Repository, pr profile.Repository) UiSystemHandlerGroup { + return UiSystemHandlerGroup{sr, pr} +} + +// Overview will list all systems. +func (h UiSystemHandlerGroup) Overview(w http.ResponseWriter, r *http.Request) { + systems, err := h.systemRepo.GetSystems() + if err != nil { + renderError(w, err) + return + } + + d := templateData{Title: "Systems", Data: systems} + renderTemplate(w, "systems/overview", d) +} + +// Show will show information about a single system. +func (h UiSystemHandlerGroup) Show(w http.ResponseWriter, r *http.Request) { + systemId, err := handlers.GetUUIDFromRequest(r) + if err != nil { + renderError(w, err) + return + } + + s, err := h.systemRepo.GetSystemById(systemId) + if err != nil { + renderError(w, err) + return + } + + p, err := h.profileRepo.GetProfileById(s.Profile) + if err != nil { + renderError(w, err) + return + } + + d := templateData{Title: "System information", Data: struct { + System system.System + Profile profile.Profile + }{ + System: s, + Profile: p, + }} + renderTemplate(w, "systems/show", d) +} + +// Create shows the page for creating a new system. +func (h UiSystemHandlerGroup) Create(w http.ResponseWriter, r *http.Request) { + d := templateData{Title: "Create system"} + renderTemplate(w, "systems/create", d) +} + +// Store will store a newly created system. +func (h UiSystemHandlerGroup) Store(w http.ResponseWriter, r *http.Request) { + s, err := parseSystemFromPostForm(r) + if err != nil { + renderError(w, err) + return + } + + s.Id = uuid.New() + + err = h.systemRepo.SetSystem(s) + if err != nil { + renderError(w, err) + return + } + + http.Redirect(w, r, "/ui/systems/"+s.Id.String(), http.StatusSeeOther) +} + +// Edit shows the page for editing an existing system. +func (h UiSystemHandlerGroup) Edit(w http.ResponseWriter, r *http.Request) { + systemId, err := handlers.GetUUIDFromRequest(r) + if err != nil { + renderError(w, err) + return + } + + s, err := h.systemRepo.GetSystemById(systemId) + if err != nil { + renderError(w, err) + return + } + + profiles, err := h.profileRepo.GetProfiles() + if err != nil { + renderError(w, err) + return + } + + d := templateData{Title: "Edit system", Data: struct { + System system.System + Profiles []profile.Profile + }{ + System: s, + Profiles: profiles, + }} + renderTemplate(w, "systems/edit", d) +} + +// Update will update the specified system. +func (h UiSystemHandlerGroup) Update(w http.ResponseWriter, r *http.Request) { + systemId, err := handlers.GetUUIDFromRequest(r) + if err != nil { + renderError(w, err) + return + } + + s, err := parseSystemFromPostForm(r) + if err != nil { + renderError(w, err) + return + } + + s.Id = systemId + + err = h.systemRepo.SetSystem(s) + if err != nil { + renderError(w, err) + return + } + + http.Redirect(w, r, "/ui/systems/"+s.Id.String(), http.StatusSeeOther) +} + +// Delete will delete the specified system. +func (h UiSystemHandlerGroup) Delete(w http.ResponseWriter, r *http.Request) { + systemId, err := handlers.GetUUIDFromRequest(r) + if err != nil { + renderError(w, err) + return + } + + err = h.systemRepo.DeleteSystemById(systemId) + if err != nil { + renderError(w, err) + return + } + + http.Redirect(w, r, "/ui/systems", http.StatusSeeOther) +} diff --git a/server/handlers/ui_handlers/template.go b/server/handlers/ui_handlers/template.go new file mode 100644 index 0000000..f89f59e --- /dev/null +++ b/server/handlers/ui_handlers/template.go @@ -0,0 +1,50 @@ +package ui_handlers + +import ( + "github.com/evanebb/gobble/resources" + "html/template" + "io" +) + +type templateData struct { + Title string + Data any + DisableNavbar bool +} + +// renderTemplateErr will render the template referenced by the passed name and return an error if it encounters one +func renderTemplateErr(w io.Writer, templateName string, d templateData) error { + var err error + + templateFile := "templates/" + templateName + ".gohtml" + tmpl, err := template.New("base").ParseFS(resources.Templates, "templates/base.gohtml", templateFile) + if err != nil { + return err + } + + err = tmpl.Execute(w, d) + if err != nil { + return err + } + + return nil +} + +// renderTemplate will render the template referenced by the passed name and render an error page if an error occurs +func renderTemplate(w io.Writer, templateName string, d templateData) { + err := renderTemplateErr(w, templateName, d) + if err != nil { + renderError(w, err) + } +} + +// renderError will render the error template with the message from the given error +func renderError(w io.Writer, err error) { + data := templateData{ + Title: "Error", + Data: err.Error(), + } + + // If we get to the point that we can't even render the error template, just do nothing + _ = renderTemplateErr(w, "error", data) +} diff --git a/server/routes.go b/server/routes.go index e97f535..62f37cf 100644 --- a/server/routes.go +++ b/server/routes.go @@ -2,58 +2,105 @@ package server import ( "github.com/evanebb/gobble/api/auth" + "github.com/evanebb/gobble/resources" "github.com/evanebb/gobble/server/handlers" + "github.com/evanebb/gobble/server/handlers/api_handlers" + "github.com/evanebb/gobble/server/handlers/ui_handlers" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "net/http" ) func (s *Server) routes() { - s.router.Use(middleware.Logger) + s.router.Use(handlers.MethodOverride, middleware.Logger) - s.router.Get("/", handlers.ErrorHandler(handlers.IndexHandler)) + // API route group s.router.Route("/api", func(r chi.Router) { - r.Use(auth.BasicAuth(s.apiUserRepo)) - r.NotFound(handlers.ErrorHandler(handlers.UnknownEndpointHandler)) + r.Use(auth.ApiBasicAuth(s.apiUserRepo)) + r.NotFound(api_handlers.ErrorHandler(api_handlers.UnknownEndpointHandler)) r.Route("/profiles", func(r chi.Router) { - h := handlers.NewProfileHandlerGroup(s.profileRepo) + h := api_handlers.NewProfileHandlerGroup(s.profileRepo) - r.Get("/", handlers.ErrorHandler(h.GetProfiles)) - r.Post("/", handlers.ErrorHandler(h.CreateProfile)) + r.Get("/", api_handlers.ErrorHandler(h.GetProfiles)) + r.Post("/", api_handlers.ErrorHandler(h.CreateProfile)) r.Route("/{uuid}", func(r chi.Router) { - r.Get("/", handlers.ErrorHandler(h.GetProfile)) - r.Put("/", handlers.ErrorHandler(h.PutProfile)) - r.Patch("/", handlers.ErrorHandler(h.PatchProfile)) - r.Delete("/", handlers.ErrorHandler(h.DeleteProfile)) + r.Get("/", api_handlers.ErrorHandler(h.GetProfile)) + r.Put("/", api_handlers.ErrorHandler(h.PutProfile)) + r.Patch("/", api_handlers.ErrorHandler(h.PatchProfile)) + r.Delete("/", api_handlers.ErrorHandler(h.DeleteProfile)) }) }) r.Route("/systems", func(r chi.Router) { - h := handlers.NewSystemHandlerGroup(s.systemRepo) + h := api_handlers.NewSystemHandlerGroup(s.systemRepo) - r.Get("/", handlers.ErrorHandler(h.GetSystems)) - r.Post("/", handlers.ErrorHandler(h.CreateSystem)) + r.Get("/", api_handlers.ErrorHandler(h.GetSystems)) + r.Post("/", api_handlers.ErrorHandler(h.CreateSystem)) r.Route("/{uuid}", func(r chi.Router) { - r.Get("/", handlers.ErrorHandler(h.GetSystem)) - r.Put("/", handlers.ErrorHandler(h.PutSystem)) - r.Patch("/", handlers.ErrorHandler(h.PatchSystem)) - r.Delete("/", handlers.ErrorHandler(h.DeleteSystem)) + r.Get("/", api_handlers.ErrorHandler(h.GetSystem)) + r.Put("/", api_handlers.ErrorHandler(h.PutSystem)) + r.Patch("/", api_handlers.ErrorHandler(h.PatchSystem)) + r.Delete("/", api_handlers.ErrorHandler(h.DeleteSystem)) }) }) r.Route("/users", func(r chi.Router) { - h := handlers.NewApiUserHandlerGroup(s.apiUserRepo) + h := api_handlers.NewApiUserHandlerGroup(s.apiUserRepo) - r.Get("/", handlers.ErrorHandler(h.GetUsers)) - r.Post("/", handlers.ErrorHandler(h.CreateUser)) + r.Get("/", api_handlers.ErrorHandler(h.GetUsers)) + r.Post("/", api_handlers.ErrorHandler(h.CreateUser)) r.Route("/{uuid}", func(r chi.Router) { - r.Get("/", handlers.ErrorHandler(h.GetUser)) - r.Put("/", handlers.ErrorHandler(h.PutUser)) - r.Delete("/", handlers.ErrorHandler(h.DeleteUser)) + r.Get("/", api_handlers.ErrorHandler(h.GetUser)) + r.Put("/", api_handlers.ErrorHandler(h.PutUser)) + r.Delete("/", api_handlers.ErrorHandler(h.DeleteUser)) }) }) }) + // This endpoint should not have authentication, so it lives outside the /api group above - h := handlers.NewPxeConfigHandlerGroup(s.systemRepo, s.profileRepo) - s.router.Get("/api/pxe-config", handlers.ErrorHandler(h.GetPxeConfig)) + h := api_handlers.NewPxeConfigHandlerGroup(s.systemRepo, s.profileRepo) + s.router.Get("/api/pxe-config", api_handlers.ErrorHandler(h.GetPxeConfig)) + + // Redirect the index to the UI by default + s.router.Handle("/", http.RedirectHandler("ui/", http.StatusMovedPermanently)) + + // Front-end (UI) routes + s.router.Route("/ui/", func(r chi.Router) { + r.Use(auth.BrowserBasicAuth(s.apiUserRepo)) + + r.NotFound(ui_handlers.PageNotFound) + r.Get("/", ui_handlers.HomePage) + r.Handle("/static/*", http.StripPrefix("/ui/", http.FileServer(http.FS(resources.Static)))) + + r.Route("/profiles", func(r chi.Router) { + h := ui_handlers.NewUiProfileHandlerGroup(s.profileRepo) + + r.Get("/", h.Overview) + r.Get("/create", h.Create) + r.Post("/", h.Store) + + r.Route("/{uuid}", func(r chi.Router) { + r.Get("/", h.Show) + r.Get("/edit", h.Edit) + r.Put("/", h.Update) + r.Delete("/", h.Delete) + }) + }) + + r.Route("/systems", func(r chi.Router) { + h := ui_handlers.NewUiSystemHandlerGroup(s.systemRepo, s.profileRepo) + + r.Get("/", h.Overview) + r.Get("/create", h.Create) + r.Post("/", h.Store) + + r.Route("/{uuid}", func(r chi.Router) { + r.Get("/", h.Show) + r.Get("/edit", h.Edit) + r.Put("/", h.Update) + r.Delete("/", h.Delete) + }) + }) + }) } From 34665b597a0365d0d6b474cb05b9e7f69f8b7697 Mon Sep 17 00:00:00 2001 From: evanebb <78433178+evanebb@users.noreply.github.com> Date: Sat, 6 Jan 2024 22:25:32 +0100 Subject: [PATCH 2/8] Remove unused PXE config textarea --- resources/templates/systems/show.gohtml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/resources/templates/systems/show.gohtml b/resources/templates/systems/show.gohtml index 6872d8f..8d37c2d 100644 --- a/resources/templates/systems/show.gohtml +++ b/resources/templates/systems/show.gohtml @@ -30,10 +30,6 @@ -
- - -
Edit From 5b32714b5ea780f549db7aee679a6cff46723c0b Mon Sep 17 00:00:00 2001 From: evanebb <78433178+evanebb@users.noreply.github.com> Date: Sat, 6 Jan 2024 23:07:52 +0100 Subject: [PATCH 3/8] Rewrite KernelParameters to use better conversion and parsing methods --- kernelparameters/kernel_parameters.go | 48 ++++++++++++------- kernelparameters/kernel_parameters_test.go | 27 +---------- repository/postgres/profile_repository.go | 6 +-- repository/postgres/system_repository.go | 8 ++-- .../handlers/api_handlers/profile_handlers.go | 10 ++-- .../handlers/api_handlers/system_handlers.go | 10 ++-- .../handlers/ui_handlers/profile_handlers.go | 3 +- .../handlers/ui_handlers/system_handlers.go | 3 +- system/pxe_config.go | 6 +-- system/pxe_config_test.go | 2 +- 10 files changed, 54 insertions(+), 69 deletions(-) diff --git a/kernelparameters/kernel_parameters.go b/kernelparameters/kernel_parameters.go index 93ece4d..f2caa18 100644 --- a/kernelparameters/kernel_parameters.go +++ b/kernelparameters/kernel_parameters.go @@ -8,7 +8,33 @@ import ( type KernelParameters map[string]string -func New(s []string) (KernelParameters, error) { +// String returns the string representation of the KernelParameters, e.g. 'initrd=initrd quiet splash' +func (k KernelParameters) String() string { + return strings.Join(k.StringSlice(), " ") +} + +// StringSlice returns the set of KernelParameters as a slice of strings, e.g. ["initrd=initrd", "quiet", "splash"] +func (k KernelParameters) StringSlice() []string { + var s []string + + for k, v := range k { + if len(v) > 0 { + s = append(s, fmt.Sprintf("%s=%s", k, v)) + } else { + s = append(s, k) + } + } + + return s +} + +// ParseString will parse and validate s into a set of KernelParameters, and return an error if it encounters an invalid kernel parameter. +func ParseString(s string) (KernelParameters, error) { + return ParseStringSlice(strings.Split(s, " ")) +} + +// ParseStringSlice will parse and validate s into a set of KernelParameters, and return an error if it encounters an invalid kernel parameter. +func ParseStringSlice(s []string) (KernelParameters, error) { kp := make(KernelParameters) for _, v := range s { @@ -19,10 +45,10 @@ func New(s []string) (KernelParameters, error) { splitStr := strings.Split(v, "=") if len(splitStr) > 1 { - // key value parameter, e.g. initrd=initrd.img + // key value parameter, e.g. 'initrd=initrd' kp[splitStr[0]] = splitStr[1] } else { - // just a value, e.g. noquiet + // just a value, e.g. 'quiet' kp[splitStr[0]] = "" } } @@ -43,21 +69,7 @@ func validateParameter(v string) error { return nil } -func FormatKernelParameters(kp KernelParameters) []string { - var s []string - - for k, v := range kp { - if len(v) > 0 { - s = append(s, fmt.Sprintf("%s=%s", k, v)) - } else { - s = append(s, k) - } - } - - return s -} - -// MergeKernelParameters merges multiple KernelParameter maps, with values from maps being overwritten in the order that they're passed into the function +// MergeKernelParameters merges multiple sets of KernelParameters, with values from maps being overwritten in the order that they're passed into the function func MergeKernelParameters(kp1 KernelParameters, kp2 ...KernelParameters) KernelParameters { for _, kpMap := range kp2 { for k, v := range kpMap { diff --git a/kernelparameters/kernel_parameters_test.go b/kernelparameters/kernel_parameters_test.go index 018ec2f..4cc1046 100644 --- a/kernelparameters/kernel_parameters_test.go +++ b/kernelparameters/kernel_parameters_test.go @@ -3,7 +3,6 @@ package kernelparameters import ( "errors" "reflect" - "sort" "testing" ) @@ -18,34 +17,12 @@ func TestNewKernelParameters(t *testing.T) { "test2": "value", } - actual, err := New(v) + actual, err := ParseStringSlice(v) if !reflect.DeepEqual(actual, expected) || err != nil { t.Fatalf(`New() = %v, %v, expected: %v, nil`, actual, err, expected) } } -func TestFormatKernelParameters(t *testing.T) { - v := KernelParameters{ - "test1": "", - "test2": "value", - } - - expected := []string{ - "test1", - "test2=value", - } - - actual := FormatKernelParameters(v) - - // Sort the slices, since the order is not guaranteed and I don't care about it either - sort.Strings(actual) - sort.Strings(expected) - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf(`FormatKernelParameters() = %v, expected: %v`, actual, expected) - } -} - func TestMergeKernelParameters(t *testing.T) { kp1 := KernelParameters{ "test1": "", @@ -82,7 +59,7 @@ func TestInvalidParameterError(t *testing.T) { expectedValue := make(KernelParameters) - actualValue, err := New(v) + actualValue, err := ParseStringSlice(v) if err == nil || !errors.As(err, &actualErr) { t.Fatalf(`New() = %v, %v, expected: %v, %v`, actualValue, actualErr, expectedValue, expectedErr) } diff --git a/repository/postgres/profile_repository.go b/repository/postgres/profile_repository.go index 55619e4..35790fe 100644 --- a/repository/postgres/profile_repository.go +++ b/repository/postgres/profile_repository.go @@ -47,7 +47,7 @@ func (r ProfileRepository) GetProfiles() ([]profile.Profile, error) { return profiles, err } - kp, err := kernelparameters.New(pp.KernelParameters) + kp, err := kernelparameters.ParseStringSlice(pp.KernelParameters) if err != nil { return profiles, err } @@ -77,7 +77,7 @@ func (r ProfileRepository) GetProfileById(id uuid.UUID) (profile.Profile, error) } // If this errors someone directly inserted garbage into the database :( - kp, err := kernelparameters.New(pp.KernelParameters) + kp, err := kernelparameters.ParseStringSlice(pp.KernelParameters) if err != nil { return pr, err } @@ -87,7 +87,7 @@ func (r ProfileRepository) GetProfileById(id uuid.UUID) (profile.Profile, error) func (r ProfileRepository) SetProfile(p profile.Profile) error { stmt := "INSERT INTO profile (uuid, name, description, kernel, initrd, kernelParameters) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (uuid) DO UPDATE set name = $2, description = $3, kernel = $4, initrd = $5, kernelParameters = $6" - _, err := r.db.Exec(context.Background(), stmt, p.Id, p.Name, p.Description, p.Kernel, p.Initrd, kernelparameters.FormatKernelParameters(p.KernelParameters)) + _, err := r.db.Exec(context.Background(), stmt, p.Id, p.Name, p.Description, p.Kernel, p.Initrd, p.KernelParameters.StringSlice()) if err != nil { return err } diff --git a/repository/postgres/system_repository.go b/repository/postgres/system_repository.go index 21f74fb..1e56403 100644 --- a/repository/postgres/system_repository.go +++ b/repository/postgres/system_repository.go @@ -48,7 +48,7 @@ func (r SystemRepository) GetSystems() ([]system.System, error) { return systems, err } - kp, err := kernelparameters.New(ps.KernelParameters) + kp, err := kernelparameters.ParseStringSlice(ps.KernelParameters) if err != nil { return systems, err } @@ -78,7 +78,7 @@ func (r SystemRepository) GetSystemByMacAddress(mac net.HardwareAddr) (system.Sy } // If this errors someone directly inserted garbage into the database :( - kp, err := kernelparameters.New(ps.KernelParameters) + kp, err := kernelparameters.ParseStringSlice(ps.KernelParameters) if err != nil { return sys, err } @@ -100,7 +100,7 @@ func (r SystemRepository) GetSystemById(id uuid.UUID) (system.System, error) { } // If this errors someone directly inserted garbage into the database :( - kp, err := kernelparameters.New(ps.KernelParameters) + kp, err := kernelparameters.ParseStringSlice(ps.KernelParameters) if err != nil { return sys, err } @@ -110,7 +110,7 @@ func (r SystemRepository) GetSystemById(id uuid.UUID) (system.System, error) { func (r SystemRepository) SetSystem(s system.System) error { stmt := "INSERT INTO system (uuid, name, description, profile, mac, kernelParameters) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (uuid) DO UPDATE set name = $2, description = $3, profile = $4, mac = $5, kernelParameters = $6" - _, err := r.db.Exec(context.Background(), stmt, s.Id, s.Name, s.Description, s.Profile, s.Mac, kernelparameters.FormatKernelParameters(s.KernelParameters)) + _, err := r.db.Exec(context.Background(), stmt, s.Id, s.Name, s.Description, s.Profile, s.Mac, s.KernelParameters.StringSlice()) if err != nil { return err } diff --git a/server/handlers/api_handlers/profile_handlers.go b/server/handlers/api_handlers/profile_handlers.go index 63b34c5..ee68b59 100644 --- a/server/handlers/api_handlers/profile_handlers.go +++ b/server/handlers/api_handlers/profile_handlers.go @@ -43,7 +43,7 @@ func newProfileResponse(p profile.Profile) profileResponse { Description: p.Description, Kernel: p.Kernel, Initrd: p.Initrd, - KernelParameters: kernelparameters.FormatKernelParameters(p.KernelParameters), + KernelParameters: p.KernelParameters.StringSlice(), } } @@ -102,7 +102,7 @@ func (h ProfileHandlerGroup) CreateProfile(w http.ResponseWriter, r *http.Reques profileId := uuid.New() - kp, err := kernelparameters.New(req.KernelParameters) + kp, err := kernelparameters.ParseStringSlice(req.KernelParameters) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } @@ -135,7 +135,7 @@ func (h ProfileHandlerGroup) PutProfile(w http.ResponseWriter, r *http.Request) return NewHTTPError(err, http.StatusBadRequest) } - kp, err := kernelparameters.New(req.KernelParameters) + kp, err := kernelparameters.ParseStringSlice(req.KernelParameters) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } @@ -170,7 +170,7 @@ func (h ProfileHandlerGroup) PatchProfile(w http.ResponseWriter, r *http.Request Description: p.Description, Kernel: p.Kernel, Initrd: p.Initrd, - KernelParameters: kernelparameters.FormatKernelParameters(p.KernelParameters), + KernelParameters: p.KernelParameters.StringSlice(), } // Decode the request body into the current profile; @@ -183,7 +183,7 @@ func (h ProfileHandlerGroup) PatchProfile(w http.ResponseWriter, r *http.Request return NewHTTPError(err, http.StatusBadRequest) } - kp, err := kernelparameters.New(req.KernelParameters) + kp, err := kernelparameters.ParseStringSlice(req.KernelParameters) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } diff --git a/server/handlers/api_handlers/system_handlers.go b/server/handlers/api_handlers/system_handlers.go index 3cdf34d..08bb14f 100644 --- a/server/handlers/api_handlers/system_handlers.go +++ b/server/handlers/api_handlers/system_handlers.go @@ -44,7 +44,7 @@ func newSystemResponse(sys system.System) systemResponse { Description: sys.Description, Profile: sys.Profile, Mac: sys.Mac.String(), - KernelParameters: kernelparameters.FormatKernelParameters(sys.KernelParameters), + KernelParameters: sys.KernelParameters.StringSlice(), } } @@ -103,7 +103,7 @@ func (h SystemHandlerGroup) CreateSystem(w http.ResponseWriter, r *http.Request) systemId := uuid.New() - kp, err := kernelparameters.New(req.KernelParameters) + kp, err := kernelparameters.ParseStringSlice(req.KernelParameters) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } @@ -141,7 +141,7 @@ func (h SystemHandlerGroup) PutSystem(w http.ResponseWriter, r *http.Request) er return NewHTTPError(err, http.StatusBadRequest) } - kp, err := kernelparameters.New(req.KernelParameters) + kp, err := kernelparameters.ParseStringSlice(req.KernelParameters) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } @@ -181,7 +181,7 @@ func (h SystemHandlerGroup) PatchSystem(w http.ResponseWriter, r *http.Request) Description: sys.Description, Profile: sys.Profile, Mac: sys.Mac.String(), - KernelParameters: kernelparameters.FormatKernelParameters(sys.KernelParameters), + KernelParameters: sys.KernelParameters.StringSlice(), } // Decode the request body into the current system; @@ -194,7 +194,7 @@ func (h SystemHandlerGroup) PatchSystem(w http.ResponseWriter, r *http.Request) return NewHTTPError(err, http.StatusBadRequest) } - kp, err := kernelparameters.New(req.KernelParameters) + kp, err := kernelparameters.ParseStringSlice(req.KernelParameters) if err != nil { return NewHTTPError(err, http.StatusBadRequest) } diff --git a/server/handlers/ui_handlers/profile_handlers.go b/server/handlers/ui_handlers/profile_handlers.go index 10555a5..3fc0e2a 100644 --- a/server/handlers/ui_handlers/profile_handlers.go +++ b/server/handlers/ui_handlers/profile_handlers.go @@ -7,7 +7,6 @@ import ( "github.com/evanebb/gobble/server/handlers" "github.com/google/uuid" "net/http" - "strings" ) func parseProfileFromPostForm(r *http.Request) (profile.Profile, error) { @@ -25,7 +24,7 @@ func parseProfileFromPostForm(r *http.Request) (profile.Profile, error) { } } - kp, err := kernelparameters.New(strings.Split(r.PostFormValue("kernelParameters"), " ")) + kp, err := kernelparameters.ParseString(r.PostFormValue("kernelParameters")) if err != nil { return p, err } diff --git a/server/handlers/ui_handlers/system_handlers.go b/server/handlers/ui_handlers/system_handlers.go index eec40af..c48c7c4 100644 --- a/server/handlers/ui_handlers/system_handlers.go +++ b/server/handlers/ui_handlers/system_handlers.go @@ -9,7 +9,6 @@ import ( "github.com/google/uuid" "net" "net/http" - "strings" ) func parseSystemFromPostForm(r *http.Request) (system.System, error) { @@ -27,7 +26,7 @@ func parseSystemFromPostForm(r *http.Request) (system.System, error) { } } - kp, err := kernelparameters.New(strings.Split(r.PostFormValue("kernelParameters"), " ")) + kp, err := kernelparameters.ParseString(r.PostFormValue("kernelParameters")) if err != nil { return s, err } diff --git a/system/pxe_config.go b/system/pxe_config.go index 7a5a8ea..836fb5a 100644 --- a/system/pxe_config.go +++ b/system/pxe_config.go @@ -3,7 +3,6 @@ package system import ( "fmt" "github.com/evanebb/gobble/kernelparameters" - "strings" ) // TODO: create a PxeConfig interface with a Render() method, have the System struct implement it @@ -41,9 +40,8 @@ func NewPxeConfig(kernel string, initrd string, kernelParameters kernelparameter } func (p PxeConfig) Render() string { - kp := kernelparameters.FormatKernelParameters(p.KernelParameters) - kpStr := strings.Join(kp, " ") - return fmt.Sprintf(template, p.Kernel, kpStr, p.Initrd) + kp := p.KernelParameters.String() + return fmt.Sprintf(template, p.Kernel, kp, p.Initrd) } func RenderNotFound() string { diff --git a/system/pxe_config_test.go b/system/pxe_config_test.go index 1497397..eac429f 100644 --- a/system/pxe_config_test.go +++ b/system/pxe_config_test.go @@ -14,7 +14,7 @@ initrd testinitrd boot ` - kp, err := kernelparameters.New([]string{"param1"}) + kp, err := kernelparameters.ParseStringSlice([]string{"param1"}) if err != nil { t.Fatalf(`NewPxeConfig(): failed to instantiate KernelParameters, error: %v`, err) } From a4656c06989a36c29089ce35dc46693bcb877694 Mon Sep 17 00:00:00 2001 From: evanebb <78433178+evanebb@users.noreply.github.com> Date: Sat, 6 Jan 2024 23:17:58 +0100 Subject: [PATCH 4/8] Use String method in templates to format kernel parameters --- resources/templates/profiles/edit.gohtml | 2 +- resources/templates/profiles/show.gohtml | 2 +- resources/templates/systems/edit.gohtml | 5 +++-- resources/templates/systems/show.gohtml | 5 +++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/resources/templates/profiles/edit.gohtml b/resources/templates/profiles/edit.gohtml index 353f13f..62dc779 100644 --- a/resources/templates/profiles/edit.gohtml +++ b/resources/templates/profiles/edit.gohtml @@ -26,7 +26,7 @@
+ value="{{.KernelParameters.String}}">
Cancel diff --git a/resources/templates/profiles/show.gohtml b/resources/templates/profiles/show.gohtml index fd4a9fe..89db103 100644 --- a/resources/templates/profiles/show.gohtml +++ b/resources/templates/profiles/show.gohtml @@ -25,7 +25,7 @@
+ value="{{.KernelParameters.String}}">
diff --git a/resources/templates/systems/edit.gohtml b/resources/templates/systems/edit.gohtml index ef17f50..61b69ef 100644 --- a/resources/templates/systems/edit.gohtml +++ b/resources/templates/systems/edit.gohtml @@ -21,7 +21,8 @@ + value="{{.System.KernelParameters.String}}"> Cancel diff --git a/resources/templates/systems/show.gohtml b/resources/templates/systems/show.gohtml index 8d37c2d..8bcef27 100644 --- a/resources/templates/systems/show.gohtml +++ b/resources/templates/systems/show.gohtml @@ -17,7 +17,8 @@
@@ -28,7 +29,7 @@
+ value="{{.System.KernelParameters.String}}">
From cb1ebe9d828b1b34b934865fcd048b688306e30d Mon Sep 17 00:00:00 2001 From: evanebb <78433178+evanebb@users.noreply.github.com> Date: Sat, 6 Jan 2024 23:31:48 +0100 Subject: [PATCH 5/8] Update tests to reflect new KernelParameters methods --- kernelparameters/kernel_parameters_test.go | 66 +++++++++++++++------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/kernelparameters/kernel_parameters_test.go b/kernelparameters/kernel_parameters_test.go index 4cc1046..c2ee184 100644 --- a/kernelparameters/kernel_parameters_test.go +++ b/kernelparameters/kernel_parameters_test.go @@ -6,7 +6,35 @@ import ( "testing" ) -func TestNewKernelParameters(t *testing.T) { +func TestParseString(t *testing.T) { + v := "test1 test2=value" + + expected := KernelParameters{ + "test1": "", + "test2": "value", + } + + actual, err := ParseString(v) + if !reflect.DeepEqual(actual, expected) || err != nil { + t.Fatalf(`ParseString() = %v, %v, expected: %v, nil`, actual, err, expected) + } +} + +func TestParseStringInvalidParameter(t *testing.T) { + var actualErr *InvalidParameterError + expectedErr := NewInvalidParameterError("===invalid") + + v := "===invalid test2=value" + + expectedValue := make(KernelParameters) + + actualValue, err := ParseString(v) + if err == nil || !errors.As(err, &actualErr) { + t.Fatalf(`ParseString() = %v, %v, expected: %v, %v`, actualValue, actualErr, expectedValue, expectedErr) + } +} + +func TestParseStringSlice(t *testing.T) { v := []string{ "test1", "test2=value", @@ -19,7 +47,24 @@ func TestNewKernelParameters(t *testing.T) { actual, err := ParseStringSlice(v) if !reflect.DeepEqual(actual, expected) || err != nil { - t.Fatalf(`New() = %v, %v, expected: %v, nil`, actual, err, expected) + t.Fatalf(`ParseStringSlice() = %v, %v, expected: %v, nil`, actual, err, expected) + } +} + +func TestParseStringSliceInvalidParameter(t *testing.T) { + var actualErr *InvalidParameterError + expectedErr := NewInvalidParameterError("this is invalid") + + v := []string{ + "this is invalid", + "test2=value", + } + + expectedValue := make(KernelParameters) + + actualValue, err := ParseStringSlice(v) + if err == nil || !errors.As(err, &actualErr) { + t.Fatalf(`ParseStringSlice() = %v, %v, expected: %v, %v`, actualValue, actualErr, expectedValue, expectedErr) } } @@ -47,20 +92,3 @@ func TestMergeKernelParameters(t *testing.T) { t.Fatalf(`MergeKernelParameters() = %v, expected: %v`, actual, expected) } } - -func TestInvalidParameterError(t *testing.T) { - var actualErr *InvalidParameterError - expectedErr := NewInvalidParameterError("this is invalid") - - v := []string{ - "this is invalid", - "test2=value", - } - - expectedValue := make(KernelParameters) - - actualValue, err := ParseStringSlice(v) - if err == nil || !errors.As(err, &actualErr) { - t.Fatalf(`New() = %v, %v, expected: %v, %v`, actualValue, actualErr, expectedValue, expectedErr) - } -} From a1445618302c17e4d03d0f952a2dec7ce8c2d261 Mon Sep 17 00:00:00 2001 From: evanebb <78433178+evanebb@users.noreply.github.com> Date: Sun, 7 Jan 2024 00:40:14 +0100 Subject: [PATCH 6/8] Move error templates to subdirectory --- resources/templates/{ => errors}/404.gohtml | 0 resources/templates/errors/500.gohtml | 9 +++++++++ 2 files changed, 9 insertions(+) rename resources/templates/{ => errors}/404.gohtml (100%) create mode 100644 resources/templates/errors/500.gohtml diff --git a/resources/templates/404.gohtml b/resources/templates/errors/404.gohtml similarity index 100% rename from resources/templates/404.gohtml rename to resources/templates/errors/404.gohtml diff --git a/resources/templates/errors/500.gohtml b/resources/templates/errors/500.gohtml new file mode 100644 index 0000000..c088995 --- /dev/null +++ b/resources/templates/errors/500.gohtml @@ -0,0 +1,9 @@ +{{ define "content" }} +
+
+

500

+

An unexpected error occurred. Please check the logs for more information.

+ Go to homepage +
+
+{{ end }} \ No newline at end of file From 5a8a95a15a347b9e694802df1be3ffb5fe3a84cc Mon Sep 17 00:00:00 2001 From: evanebb <78433178+evanebb@users.noreply.github.com> Date: Sun, 7 Jan 2024 00:41:40 +0100 Subject: [PATCH 7/8] Render generic error page instead of showing error message --- server/handlers/ui_handlers/handlers.go | 2 +- .../handlers/ui_handlers/profile_handlers.go | 24 ++++++++-------- .../handlers/ui_handlers/system_handlers.go | 28 +++++++++---------- server/handlers/ui_handlers/template.go | 16 +++++------ 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/server/handlers/ui_handlers/handlers.go b/server/handlers/ui_handlers/handlers.go index 0e1b6ea..ae34148 100644 --- a/server/handlers/ui_handlers/handlers.go +++ b/server/handlers/ui_handlers/handlers.go @@ -4,7 +4,7 @@ import "net/http" func PageNotFound(w http.ResponseWriter, r *http.Request) { d := templateData{Title: "Page not found", DisableNavbar: true} - renderTemplate(w, "404", d) + renderTemplate(w, "errors/404", d) } func HomePage(w http.ResponseWriter, r *http.Request) { diff --git a/server/handlers/ui_handlers/profile_handlers.go b/server/handlers/ui_handlers/profile_handlers.go index 3fc0e2a..a38b69a 100644 --- a/server/handlers/ui_handlers/profile_handlers.go +++ b/server/handlers/ui_handlers/profile_handlers.go @@ -52,7 +52,7 @@ func NewUiProfileHandlerGroup(pr profile.Repository) UiProfileHandlerGroup { func (h UiProfileHandlerGroup) Overview(w http.ResponseWriter, r *http.Request) { profiles, err := h.profileRepo.GetProfiles() if err != nil { - renderError(w, err) + renderError(w) return } @@ -64,13 +64,13 @@ func (h UiProfileHandlerGroup) Overview(w http.ResponseWriter, r *http.Request) func (h UiProfileHandlerGroup) Show(w http.ResponseWriter, r *http.Request) { profileId, err := handlers.GetUUIDFromRequest(r) if err != nil { - renderError(w, err) + renderError(w) return } p, err := h.profileRepo.GetProfileById(profileId) if err != nil { - renderError(w, err) + renderError(w) return } @@ -88,7 +88,7 @@ func (h UiProfileHandlerGroup) Create(w http.ResponseWriter, r *http.Request) { func (h UiProfileHandlerGroup) Store(w http.ResponseWriter, r *http.Request) { p, err := parseProfileFromPostForm(r) if err != nil { - renderError(w, err) + renderError(w) return } @@ -96,7 +96,7 @@ func (h UiProfileHandlerGroup) Store(w http.ResponseWriter, r *http.Request) { err = h.profileRepo.SetProfile(p) if err != nil { - renderError(w, err) + renderError(w) return } @@ -107,13 +107,13 @@ func (h UiProfileHandlerGroup) Store(w http.ResponseWriter, r *http.Request) { func (h UiProfileHandlerGroup) Edit(w http.ResponseWriter, r *http.Request) { profileId, err := handlers.GetUUIDFromRequest(r) if err != nil { - renderError(w, err) + renderError(w) return } p, err := h.profileRepo.GetProfileById(profileId) if err != nil { - renderError(w, err) + renderError(w) return } @@ -125,13 +125,13 @@ func (h UiProfileHandlerGroup) Edit(w http.ResponseWriter, r *http.Request) { func (h UiProfileHandlerGroup) Update(w http.ResponseWriter, r *http.Request) { profileId, err := handlers.GetUUIDFromRequest(r) if err != nil { - renderError(w, err) + renderError(w) return } p, err := parseProfileFromPostForm(r) if err != nil { - renderError(w, err) + renderError(w) return } @@ -139,7 +139,7 @@ func (h UiProfileHandlerGroup) Update(w http.ResponseWriter, r *http.Request) { err = h.profileRepo.SetProfile(p) if err != nil { - renderError(w, err) + renderError(w) return } @@ -150,13 +150,13 @@ func (h UiProfileHandlerGroup) Update(w http.ResponseWriter, r *http.Request) { func (h UiProfileHandlerGroup) Delete(w http.ResponseWriter, r *http.Request) { profileId, err := handlers.GetUUIDFromRequest(r) if err != nil { - renderError(w, err) + renderError(w) return } err = h.profileRepo.DeleteProfileById(profileId) if err != nil { - renderError(w, err) + renderError(w) return } diff --git a/server/handlers/ui_handlers/system_handlers.go b/server/handlers/ui_handlers/system_handlers.go index c48c7c4..b3f0750 100644 --- a/server/handlers/ui_handlers/system_handlers.go +++ b/server/handlers/ui_handlers/system_handlers.go @@ -65,7 +65,7 @@ func NewUiSystemHandlerGroup(sr system.Repository, pr profile.Repository) UiSyst func (h UiSystemHandlerGroup) Overview(w http.ResponseWriter, r *http.Request) { systems, err := h.systemRepo.GetSystems() if err != nil { - renderError(w, err) + renderError(w) return } @@ -77,19 +77,19 @@ func (h UiSystemHandlerGroup) Overview(w http.ResponseWriter, r *http.Request) { func (h UiSystemHandlerGroup) Show(w http.ResponseWriter, r *http.Request) { systemId, err := handlers.GetUUIDFromRequest(r) if err != nil { - renderError(w, err) + renderError(w) return } s, err := h.systemRepo.GetSystemById(systemId) if err != nil { - renderError(w, err) + renderError(w) return } p, err := h.profileRepo.GetProfileById(s.Profile) if err != nil { - renderError(w, err) + renderError(w) return } @@ -113,7 +113,7 @@ func (h UiSystemHandlerGroup) Create(w http.ResponseWriter, r *http.Request) { func (h UiSystemHandlerGroup) Store(w http.ResponseWriter, r *http.Request) { s, err := parseSystemFromPostForm(r) if err != nil { - renderError(w, err) + renderError(w) return } @@ -121,7 +121,7 @@ func (h UiSystemHandlerGroup) Store(w http.ResponseWriter, r *http.Request) { err = h.systemRepo.SetSystem(s) if err != nil { - renderError(w, err) + renderError(w) return } @@ -132,19 +132,19 @@ func (h UiSystemHandlerGroup) Store(w http.ResponseWriter, r *http.Request) { func (h UiSystemHandlerGroup) Edit(w http.ResponseWriter, r *http.Request) { systemId, err := handlers.GetUUIDFromRequest(r) if err != nil { - renderError(w, err) + renderError(w) return } s, err := h.systemRepo.GetSystemById(systemId) if err != nil { - renderError(w, err) + renderError(w) return } profiles, err := h.profileRepo.GetProfiles() if err != nil { - renderError(w, err) + renderError(w) return } @@ -162,13 +162,13 @@ func (h UiSystemHandlerGroup) Edit(w http.ResponseWriter, r *http.Request) { func (h UiSystemHandlerGroup) Update(w http.ResponseWriter, r *http.Request) { systemId, err := handlers.GetUUIDFromRequest(r) if err != nil { - renderError(w, err) + renderError(w) return } s, err := parseSystemFromPostForm(r) if err != nil { - renderError(w, err) + renderError(w) return } @@ -176,7 +176,7 @@ func (h UiSystemHandlerGroup) Update(w http.ResponseWriter, r *http.Request) { err = h.systemRepo.SetSystem(s) if err != nil { - renderError(w, err) + renderError(w) return } @@ -187,13 +187,13 @@ func (h UiSystemHandlerGroup) Update(w http.ResponseWriter, r *http.Request) { func (h UiSystemHandlerGroup) Delete(w http.ResponseWriter, r *http.Request) { systemId, err := handlers.GetUUIDFromRequest(r) if err != nil { - renderError(w, err) + renderError(w) return } err = h.systemRepo.DeleteSystemById(systemId) if err != nil { - renderError(w, err) + renderError(w) return } diff --git a/server/handlers/ui_handlers/template.go b/server/handlers/ui_handlers/template.go index f89f59e..e68a475 100644 --- a/server/handlers/ui_handlers/template.go +++ b/server/handlers/ui_handlers/template.go @@ -34,17 +34,15 @@ func renderTemplateErr(w io.Writer, templateName string, d templateData) error { func renderTemplate(w io.Writer, templateName string, d templateData) { err := renderTemplateErr(w, templateName, d) if err != nil { - renderError(w, err) + renderError(w) } } -// renderError will render the error template with the message from the given error -func renderError(w io.Writer, err error) { - data := templateData{ - Title: "Error", - Data: err.Error(), - } - +// renderError will render a generic internal server error page. +func renderError(w io.Writer) { // If we get to the point that we can't even render the error template, just do nothing - _ = renderTemplateErr(w, "error", data) + _ = renderTemplateErr(w, "errors/500", templateData{ + Title: "Error", + DisableNavbar: true, + }) } From 897840f1a682c6a33a5609ef0f5700294240a7f1 Mon Sep 17 00:00:00 2001 From: evanebb <78433178+evanebb@users.noreply.github.com> Date: Sun, 7 Jan 2024 00:48:59 +0100 Subject: [PATCH 8/8] Add dropdown for profile selection when creating new system --- resources/templates/systems/create.gohtml | 6 +++++- server/handlers/ui_handlers/system_handlers.go | 12 +++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/resources/templates/systems/create.gohtml b/resources/templates/systems/create.gohtml index f8116b7..42267d6 100644 --- a/resources/templates/systems/create.gohtml +++ b/resources/templates/systems/create.gohtml @@ -12,7 +12,11 @@
- +
diff --git a/server/handlers/ui_handlers/system_handlers.go b/server/handlers/ui_handlers/system_handlers.go index b3f0750..0c7e12e 100644 --- a/server/handlers/ui_handlers/system_handlers.go +++ b/server/handlers/ui_handlers/system_handlers.go @@ -105,7 +105,17 @@ func (h UiSystemHandlerGroup) Show(w http.ResponseWriter, r *http.Request) { // Create shows the page for creating a new system. func (h UiSystemHandlerGroup) Create(w http.ResponseWriter, r *http.Request) { - d := templateData{Title: "Create system"} + profiles, err := h.profileRepo.GetProfiles() + if err != nil { + renderError(w) + return + } + + d := templateData{Title: "Create system", Data: struct { + Profiles []profile.Profile + }{ + Profiles: profiles, + }} renderTemplate(w, "systems/create", d) }