- Added upstream objects

- Renamed host templates to nginx templates
- Generate upstream templates
- Better nginx error reporting when reloading
- Use tparse for golang test reporting
This commit is contained in:
Jamie Curnow
2023-01-04 15:36:56 +10:00
parent b3ae2f4dbb
commit 5e5f0de0e2
82 changed files with 2209 additions and 294 deletions

View File

@ -1,6 +1,7 @@
package handler
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
@ -45,13 +46,16 @@ func GetHost() func(http.ResponseWriter, *http.Request) {
return
}
hostObject, err := host.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
item, err := host.GetByID(hostID)
switch err {
case sql.ErrNoRows:
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
case nil:
// nolint: errcheck,gosec
hostObject.Expand(getExpandFromContext(r))
h.ResultResponseJSON(w, r, http.StatusOK, hostObject)
item.Expand(getExpandFromContext(r))
h.ResultResponseJSON(w, r, http.StatusOK, item)
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
}
}
@ -111,6 +115,11 @@ func UpdateHost() func(http.ResponseWriter, *http.Request) {
return
}
if err = validator.ValidateHost(hostObject); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
if err = hostObject.Save(false); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
@ -137,11 +146,14 @@ func DeleteHost() func(http.ResponseWriter, *http.Request) {
return
}
host, err := host.GetByID(hostID)
if err != nil {
item, err := host.GetByID(hostID)
switch err {
case sql.ErrNoRows:
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
case nil:
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, host.Delete())
}
}
}

View File

@ -1,6 +1,7 @@
package handler
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
@ -8,13 +9,12 @@ import (
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/entity/host"
"npm/internal/entity/hosttemplate"
"npm/internal/entity/nginxtemplate"
)
// GetHostTemplates will return a list of Host Templates
// Route: GET /host-templates
func GetHostTemplates() func(http.ResponseWriter, *http.Request) {
// GetNginxTemplates will return a list of Nginx Templates
// Route: GET /nginx-templates
func GetNginxTemplates() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
@ -22,18 +22,18 @@ func GetHostTemplates() func(http.ResponseWriter, *http.Request) {
return
}
hosts, err := hosttemplate.List(pageInfo, middleware.GetFiltersFromContext(r))
items, err := nginxtemplate.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
h.ResultResponseJSON(w, r, http.StatusOK, items)
}
}
}
// GetHostTemplate will return a single Host Template
// Route: GET /host-templates/{templateID}
func GetHostTemplate() func(http.ResponseWriter, *http.Request) {
// GetNginxTemplate will return a single Nginx Template
// Route: GET /nginx-templates/{templateID}
func GetNginxTemplate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var templateID int
@ -42,23 +42,26 @@ func GetHostTemplate() func(http.ResponseWriter, *http.Request) {
return
}
host, err := hosttemplate.GetByID(templateID)
if err != nil {
item, err := nginxtemplate.GetByID(templateID)
switch err {
case sql.ErrNoRows:
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
case nil:
h.ResultResponseJSON(w, r, http.StatusOK, item)
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, host)
}
}
}
// CreateHostTemplate will create a Host Template
// Route: POST /host-templates
func CreateHostTemplate() func(http.ResponseWriter, *http.Request) {
// CreateNginxTemplate will create a Nginx Template
// Route: POST /nginx-templates
func CreateNginxTemplate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newHostTemplate hosttemplate.Model
err := json.Unmarshal(bodyBytes, &newHostTemplate)
var newNginxTemplate nginxtemplate.Model
err := json.Unmarshal(bodyBytes, &newNginxTemplate)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
@ -66,20 +69,20 @@ func CreateHostTemplate() func(http.ResponseWriter, *http.Request) {
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
newHostTemplate.UserID = userID
newNginxTemplate.UserID = userID
if err = newHostTemplate.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host Template: %s", err.Error()), nil)
if err = newNginxTemplate.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Nginx Template: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newHostTemplate)
h.ResultResponseJSON(w, r, http.StatusOK, newNginxTemplate)
}
}
// UpdateHostTemplate updates a host template
// Route: PUT /host-templates/{templateID}
func UpdateHostTemplate() func(http.ResponseWriter, *http.Request) {
// UpdateNginxTemplate updates a nginx template
// Route: PUT /nginx-templates/{templateID}
func UpdateNginxTemplate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var templateID int
@ -90,30 +93,30 @@ func UpdateHostTemplate() func(http.ResponseWriter, *http.Request) {
// reconfigure, _ := getQueryVarBool(r, "reconfigure", false, false)
hostTemplate, err := hosttemplate.GetByID(templateID)
nginxTemplate, err := nginxtemplate.GetByID(templateID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &hostTemplate)
err := json.Unmarshal(bodyBytes, &nginxTemplate)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = hostTemplate.Save(); err != nil {
if err = nginxTemplate.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, hostTemplate)
h.ResultResponseJSON(w, r, http.StatusOK, nginxTemplate)
}
}
}
// DeleteHostTemplate removes a host template
// Route: DELETE /host-templates/{templateID}
func DeleteHostTemplate() func(http.ResponseWriter, *http.Request) {
// DeleteNginxTemplate removes a nginx template
// Route: DELETE /nginx-templates/{templateID}
func DeleteNginxTemplate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var templateID int
@ -122,11 +125,14 @@ func DeleteHostTemplate() func(http.ResponseWriter, *http.Request) {
return
}
hostTemplate, err := host.GetByID(templateID)
if err != nil {
item, err := nginxtemplate.GetByID(templateID)
switch err {
case sql.ErrNoRows:
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
case nil:
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, hostTemplate.Delete())
}
}
}

View File

@ -95,8 +95,8 @@ func replaceIncomingSchemas(swaggerSchema []byte) []byte {
str = strings.ReplaceAll(str, `"{{schema.CreateHost}}"`, schema.CreateHost())
str = strings.ReplaceAll(str, `"{{schema.UpdateHost}}"`, schema.UpdateHost())
str = strings.ReplaceAll(str, `"{{schema.CreateHostTemplate}}"`, schema.CreateHostTemplate())
str = strings.ReplaceAll(str, `"{{schema.UpdateHostTemplate}}"`, schema.UpdateHostTemplate())
str = strings.ReplaceAll(str, `"{{schema.CreateNginxTemplate}}"`, schema.CreateNginxTemplate())
str = strings.ReplaceAll(str, `"{{schema.UpdateNginxTemplate}}"`, schema.UpdateNginxTemplate())
str = strings.ReplaceAll(str, `"{{schema.CreateStream}}"`, schema.CreateStream())
str = strings.ReplaceAll(str, `"{{schema.UpdateStream}}"`, schema.UpdateStream())
@ -104,5 +104,7 @@ func replaceIncomingSchemas(swaggerSchema []byte) []byte {
str = strings.ReplaceAll(str, `"{{schema.CreateDNSProvider}}"`, schema.CreateDNSProvider())
str = strings.ReplaceAll(str, `"{{schema.UpdateDNSProvider}}"`, schema.UpdateDNSProvider())
str = strings.ReplaceAll(str, `"{{schema.CreateUpstream}}"`, schema.CreateUpstream())
return []byte(str)
}

View File

@ -0,0 +1,129 @@
package handler
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/entity/upstream"
"npm/internal/jobqueue"
"npm/internal/logger"
"npm/internal/nginx"
"npm/internal/validator"
)
// GetUpstreams will return a list of Upstreams
// Route: GET /upstreams
func GetUpstreams() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
items, err := upstream.List(pageInfo, middleware.GetFiltersFromContext(r), getExpandFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, items)
}
}
}
// GetUpstream will return a single Upstream
// Route: GET /upstreams/{upstreamID}
func GetUpstream() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var upstreamID int
if upstreamID, err = getURLParamInt(r, "upstreamID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
item, err := upstream.GetByID(upstreamID)
switch err {
case sql.ErrNoRows:
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
case nil:
// nolint: errcheck,gosec
item.Expand(getExpandFromContext(r))
h.ResultResponseJSON(w, r, http.StatusOK, item)
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
}
}
// CreateUpstream will create a Upstream
// Route: POST /upstreams
func CreateUpstream() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newUpstream upstream.Model
err := json.Unmarshal(bodyBytes, &newUpstream)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
newUpstream.UserID = userID
if err = validator.ValidateUpstream(newUpstream); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
if err = newUpstream.Save(false); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Upstream: %s", err.Error()), nil)
return
}
configureUpstream(newUpstream)
h.ResultResponseJSON(w, r, http.StatusOK, newUpstream)
}
}
// DeleteUpstream removes a upstream
// Route: DELETE /upstreams/{upstreamID}
func DeleteUpstream() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var upstreamID int
if upstreamID, err = getURLParamInt(r, "upstreamID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
item, err := upstream.GetByID(upstreamID)
switch err {
case sql.ErrNoRows:
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
case nil:
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
default:
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
}
}
func configureUpstream(u upstream.Model) {
err := jobqueue.AddJob(jobqueue.Job{
Name: "NginxConfigureUpstream",
Action: func() error {
return nginx.ConfigureUpstream(u)
},
})
if err != nil {
logger.Error("ConfigureUpstreamError", err)
}
}

View File

@ -12,9 +12,10 @@ import (
"npm/internal/entity/certificateauthority"
"npm/internal/entity/dnsprovider"
"npm/internal/entity/host"
"npm/internal/entity/hosttemplate"
"npm/internal/entity/nginxtemplate"
"npm/internal/entity/setting"
"npm/internal/entity/stream"
"npm/internal/entity/upstream"
"npm/internal/entity/user"
"npm/internal/logger"
@ -169,16 +170,16 @@ func applyRoutes(r chi.Router) chi.Router {
Put("/{hostID:[0-9]+}", handler.UpdateHost())
})
// Host Templates
r.With(middleware.EnforceSetup(true)).Route("/host-templates", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityHostTemplatesView), middleware.Filters(hosttemplate.GetFilterSchema())).
Get("/", handler.GetHostTemplates())
r.With(middleware.Enforce(user.CapabilityHostTemplatesView)).Get("/{templateID:[0-9]+}", handler.GetHostTemplates())
r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).Delete("/{templateID:[0-9]+}", handler.DeleteHostTemplate())
r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).With(middleware.EnforceRequestSchema(schema.CreateHostTemplate())).
Post("/", handler.CreateHostTemplate())
r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).With(middleware.EnforceRequestSchema(schema.UpdateHostTemplate())).
Put("/{templateID:[0-9]+}", handler.UpdateHostTemplate())
// Nginx Templates
r.With(middleware.EnforceSetup(true)).Route("/nginx-templates", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityNginxTemplatesView), middleware.Filters(nginxtemplate.GetFilterSchema())).
Get("/", handler.GetNginxTemplates())
r.With(middleware.Enforce(user.CapabilityNginxTemplatesView)).Get("/{templateID:[0-9]+}", handler.GetNginxTemplates())
r.With(middleware.Enforce(user.CapabilityNginxTemplatesManage)).Delete("/{templateID:[0-9]+}", handler.DeleteNginxTemplate())
r.With(middleware.Enforce(user.CapabilityNginxTemplatesManage)).With(middleware.EnforceRequestSchema(schema.CreateNginxTemplate())).
Post("/", handler.CreateNginxTemplate())
r.With(middleware.Enforce(user.CapabilityNginxTemplatesManage)).With(middleware.EnforceRequestSchema(schema.UpdateNginxTemplate())).
Put("/{templateID:[0-9]+}", handler.UpdateNginxTemplate())
})
// Streams
@ -192,6 +193,16 @@ func applyRoutes(r chi.Router) chi.Router {
r.With(middleware.Enforce(user.CapabilityStreamsManage)).With(middleware.EnforceRequestSchema(schema.UpdateStream())).
Put("/{hostID:[0-9]+}", handler.UpdateStream())
})
// Upstreams
r.With(middleware.EnforceSetup(true)).Route("/upstreams", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityHostsView), middleware.Filters(upstream.GetFilterSchema())).
Get("/", handler.GetUpstreams())
r.With(middleware.Enforce(user.CapabilityHostsView)).Get("/{upstreamID:[0-9]+}", handler.GetUpstream())
r.With(middleware.Enforce(user.CapabilityHostsManage)).Delete("/{upstreamID:[0-9]+}", handler.DeleteUpstream())
r.With(middleware.Enforce(user.CapabilityHostsManage)).With(middleware.EnforceRequestSchema(schema.CreateUpstream())).
Post("/", handler.CreateUpstream())
})
})
return r

View File

@ -17,14 +17,14 @@ func CreateHost() string {
"required": [
"type",
"domain_names",
"host_template_id"
"nginx_template_id"
],
"properties": {
"type": {
"type": "string",
"pattern": "^proxy$"
},
"host_template_id": {
"nginx_template_id": {
"type": "integer",
"minimum": 1
},
@ -63,9 +63,6 @@ func CreateHost() string {
"paths": {
"type": "string"
},
"upstream_options": {
"type": "string"
},
"advanced_config": {
"type": "string"
},

View File

@ -1,14 +1,14 @@
package schema
// CreateHostTemplate is the schema for incoming data validation
func CreateHostTemplate() string {
// CreateNginxTemplate is the schema for incoming data validation
func CreateNginxTemplate() string {
return `
{
"type": "object",
"additionalProperties": false,
"required": [
"name",
"host_type",
"type",
"template"
],
"properties": {
@ -16,9 +16,9 @@ func CreateHostTemplate() string {
"type": "string",
"minLength": 1
},
"host_type": {
"type": {
"type": "string",
"pattern": "^proxy|redirect|dead|stream$"
"pattern": "^proxy|redirect|dead|stream|upstream$"
},
"template": {
"type": "string",

View File

@ -0,0 +1,73 @@
package schema
import "fmt"
// CreateUpstream is the schema for incoming data validation
func CreateUpstream() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"required": [
"name",
"servers",
"nginx_template_id"
],
"properties": {
"name": %s,
"nginx_template_id": {
"type": "integer",
"minimum": 1
},
"advanced_config": %s,
"ip_hash": {
"type": "boolean"
},
"ntlm": {
"type": "boolean"
},
"keepalive": {
"type": "integer"
},
"keepalive_requests": {
"type": "integer"
},
"keepalive_time": {
"type": "string"
},
"keepalive_timeout": {
"type": "string"
},
"servers": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"server"
],
"properties": {
"server": %s,
"weight": {
"type": "integer"
},
"max_conns": {
"type": "integer"
},
"max_fails": {
"type": "integer"
},
"fail_timeout": {
"type": "integer"
},
"backup": {
"type": "boolean"
}
}
}
}
}
}
`, stringMinMax(1, 100), stringMinMax(0, 1024), stringMinMax(2, 255))
}

View File

@ -10,7 +10,7 @@ func UpdateHost() string {
"additionalProperties": false,
"minProperties": 1,
"properties": {
"host_template_id": {
"nginx_template_id": {
"type": "integer",
"minimum": 1
},

View File

@ -1,7 +1,7 @@
package schema
// UpdateHostTemplate is the schema for incoming data validation
func UpdateHostTemplate() string {
func UpdateNginxTemplate() string {
return `
{
"type": "object",