Write host template on save

This commit is contained in:
Jamie Curnow 2022-07-21 18:02:07 +10:00
parent 5b6dbaf43e
commit 8d37f5df8d
5 changed files with 182 additions and 33 deletions

View File

@ -78,7 +78,7 @@ func CreateHost() func(http.ResponseWriter, *http.Request) {
return return
} }
if err = newHost.Save(); err != nil { if err = newHost.Save(false); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host: %s", err.Error()), nil) h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host: %s", err.Error()), nil)
return return
} }
@ -111,7 +111,7 @@ func UpdateHost() func(http.ResponseWriter, *http.Request) {
return return
} }
if err = hostObject.Save(); err != nil { if err = hostObject.Save(false); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return return
} }

View File

@ -6,6 +6,7 @@ import (
"npm/internal/database" "npm/internal/database"
"npm/internal/entity/certificate" "npm/internal/entity/certificate"
"npm/internal/entity/hosttemplate"
"npm/internal/entity/user" "npm/internal/entity/user"
"npm/internal/types" "npm/internal/types"
"npm/internal/util" "npm/internal/util"
@ -56,8 +57,9 @@ type Model struct {
IsDisabled bool `json:"is_disabled" db:"is_disabled" filter:"is_disabled,boolean"` IsDisabled bool `json:"is_disabled" db:"is_disabled" filter:"is_disabled,boolean"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
// Expansions // Expansions
Certificate *certificate.Model `json:"certificate,omitempty"` Certificate *certificate.Model `json:"certificate,omitempty"`
User *user.Model `json:"user,omitempty"` HostTemplate *hosttemplate.Model `json:"host_template,omitempty"`
User *user.Model `json:"user,omitempty"`
} }
func (m *Model) getByQuery(query string, params []interface{}) error { func (m *Model) getByQuery(query string, params []interface{}) error {
@ -82,15 +84,17 @@ func (m *Model) Touch(created bool) {
} }
// Save will save this model to the DB // Save will save this model to the DB
func (m *Model) Save() error { func (m *Model) Save(skipConfiguration bool) error {
var err error var err error
if m.UserID == 0 { if m.UserID == 0 {
return fmt.Errorf("User ID must be specified") return fmt.Errorf("User ID must be specified")
} }
// Set this host as requiring reconfiguration if !skipConfiguration {
m.Status = StatusReady // Set this host as requiring reconfiguration
m.Status = StatusReady
}
if m.ID == 0 { if m.ID == 0 {
m.ID, err = create(m) m.ID, err = create(m)
@ -105,7 +109,7 @@ func (m *Model) Save() error {
func (m *Model) Delete() bool { func (m *Model) Delete() bool {
m.Touch(false) m.Touch(false)
m.IsDeleted = true m.IsDeleted = true
if err := m.Save(); err != nil { if err := m.Save(false); err != nil {
return false return false
} }
return true return true
@ -127,5 +131,11 @@ func (m *Model) Expand(items []string) error {
m.Certificate = &cert m.Certificate = &cert
} }
if util.SliceContainsItem(items, "hosttemplate") && m.HostTemplateID > 0 {
var templ hosttemplate.Model
templ, err = hosttemplate.GetByID(m.HostTemplateID)
m.HostTemplate = &templ
}
return err return err
} }

View File

@ -1,13 +1,51 @@
package nginx package nginx
import "npm/internal/entity/host" import (
"fmt"
"npm/internal/config"
"npm/internal/entity/host"
"npm/internal/logger"
)
// ConfigureHost will attempt to write nginx conf and reload nginx // ConfigureHost will attempt to write nginx conf and reload nginx
func ConfigureHost(h host.Model) error { func ConfigureHost(h host.Model) error {
// nolint: errcheck, gosec // nolint: errcheck, gosec
h.Expand([]string{"certificate"}) h.Expand([]string{"certificate", "hosttemplate"})
data := TemplateData{
ConfDir: fmt.Sprintf("%s/nginx/hosts", config.Configuration.DataFolder),
DataDir: config.Configuration.DataFolder,
CertsDir: config.Configuration.Acmesh.CertHome,
Host: &h,
Certificate: h.Certificate,
}
filename := fmt.Sprintf("%s/host_%d.conf", data.ConfDir, h.ID)
// Write the config to disk
err := writeTemplate(filename, h.HostTemplate.Template, data)
if err != nil {
// this configuration failed somehow
h.Status = host.StatusError
h.ErrorMessage = fmt.Sprintf("Template generation failed: %s", err.Error())
logger.Debug(h.ErrorMessage)
return h.Save(true)
}
// nolint: errcheck, gosec // nolint: errcheck, gosec
reloadNginx() if err := reloadNginx(); err != nil {
return nil // reloading nginx failed, likely due to this host having a problem
h.Status = host.StatusError
h.ErrorMessage = fmt.Sprintf("Nginx configuation error: %s", err.Error())
writeConfigFile(filename, fmt.Sprintf("# %s", h.ErrorMessage))
logger.Debug(h.ErrorMessage)
} else {
// All good
h.Status = host.StatusOK
h.ErrorMessage = ""
logger.Debug("ConfigureHost OK: %+v", h)
}
return h.Save(true)
} }

View File

@ -0,0 +1,86 @@
package nginx
import (
"testing"
"npm/internal/entity/certificate"
"npm/internal/entity/host"
"github.com/stretchr/testify/assert"
)
func TestWriteTemplate(t *testing.T) {
template := `
{{#if Host.IsDisabled}}
# Host is disabled
{{else}}
server {
{{#if Certificate}}
{{#if Certificate.CertificateAuthorityID}}
# Acme SSL
include {{ConfDir}}/npm/conf.d/acme-challenge.conf;
include {{ConfDir}}/npm/conf.d/include/ssl-ciphers.conf;
ssl_certificate {{CertsDir}}/npm-{{Certificate.ID}}/fullchain.pem;
ssl_certificate_key {{CertsDir}}/npm-{{Certificate.ID}}/privkey.pem;
{{else}}
# Custom SSL
ssl_certificate {{DataDir}}/custom_ssl/npm-{{Certificate.ID}}/fullchain.pem;
ssl_certificate_key {{DataDir}}/custom_ssl/npm-{{Certificate.ID}}/privkey.pem;
{{/if}}
{{/if}}
}
{{/if}}
`
type want struct {
output string
err error
}
tests := []struct {
name string
data TemplateData
want want
}{
{
name: "Basic Template enabled",
data: TemplateData{
ConfDir: "/etc/nginx/conf.d",
Host: &host.Model{
IsDisabled: false,
},
Certificate: &certificate.Model{
CertificateAuthorityID: 0,
},
},
want: want{
output: "\nserver {\n # Custom SSL\n ssl_certificate /custom_ssl/npm-0/fullchain.pem;\n ssl_certificate_key /custom_ssl/npm-0/privkey.pem;\n \n}\n\n",
err: nil,
},
},
{
name: "Basic Template disabled",
data: TemplateData{
ConfDir: "/etc/nginx/conf.d",
DataDir: "/data",
CertsDir: "/acme.sh/certs",
Host: &host.Model{
IsDisabled: true,
},
},
want: want{
output: "\n # Host is disabled\n\n",
err: nil,
},
},
}
for _, test := range tests {
t.Run(test.name, func(st *testing.T) {
output, err := generateHostConfig(template, test.data)
assert.Equal(t, test.want.err, err)
assert.Equal(t, test.want.output, output)
})
}
}

View File

@ -1,31 +1,46 @@
package nginx package nginx
import ( import (
"io/fs" "fmt"
"io/ioutil" "io/ioutil"
"npm/embed" "npm/internal/entity/certificate"
"npm/internal/entity/host"
"npm/internal/logger"
"github.com/aymerick/raymond" "github.com/aymerick/raymond"
) )
// WriteTemplate will load, parse and write a template file // TemplateData ...
func WriteTemplate(templateName, outputFilename string, data map[string]interface{}) error { type TemplateData struct {
// get template file content ConfDir string
subFs, _ := fs.Sub(embed.NginxFiles, "nginx") DataDir string
template, err := fs.ReadFile(subFs, templateName) CertsDir string
Host *host.Model
if err != nil { Certificate *certificate.Model
return err }
}
func generateHostConfig(template string, data TemplateData) (string, error) {
// Render return raymond.Render(template, data)
parsedFile, err := raymond.Render(string(template), data) }
if err != nil {
return err func writeTemplate(filename, template string, data TemplateData) error {
} output, err := generateHostConfig(template, data)
if err != nil {
// Write it output = fmt.Sprintf("# Template Error: %s", err.Error())
// nolint: gosec }
return ioutil.WriteFile(outputFilename, []byte(parsedFile), 0644)
// Write it. This will also write an error comment if generation failed
// nolint: gosec
writeErr := writeConfigFile(filename, output)
if err != nil {
return err
}
return writeErr
}
func writeConfigFile(filename, content string) error {
logger.Debug("Writing %s with:\n%s", filename, content)
// nolint: gosec
return ioutil.WriteFile(filename, []byte(content), 0644)
} }