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
}
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)
return
}
@ -111,7 +111,7 @@ func UpdateHost() func(http.ResponseWriter, *http.Request) {
return
}
if err = hostObject.Save(); err != nil {
if err = hostObject.Save(false); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}

View File

@ -6,6 +6,7 @@ import (
"npm/internal/database"
"npm/internal/entity/certificate"
"npm/internal/entity/hosttemplate"
"npm/internal/entity/user"
"npm/internal/types"
"npm/internal/util"
@ -56,8 +57,9 @@ type Model struct {
IsDisabled bool `json:"is_disabled" db:"is_disabled" filter:"is_disabled,boolean"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
// Expansions
Certificate *certificate.Model `json:"certificate,omitempty"`
User *user.Model `json:"user,omitempty"`
Certificate *certificate.Model `json:"certificate,omitempty"`
HostTemplate *hosttemplate.Model `json:"host_template,omitempty"`
User *user.Model `json:"user,omitempty"`
}
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
func (m *Model) Save() error {
func (m *Model) Save(skipConfiguration bool) error {
var err error
if m.UserID == 0 {
return fmt.Errorf("User ID must be specified")
}
// Set this host as requiring reconfiguration
m.Status = StatusReady
if !skipConfiguration {
// Set this host as requiring reconfiguration
m.Status = StatusReady
}
if m.ID == 0 {
m.ID, err = create(m)
@ -105,7 +109,7 @@ func (m *Model) Save() error {
func (m *Model) Delete() bool {
m.Touch(false)
m.IsDeleted = true
if err := m.Save(); err != nil {
if err := m.Save(false); err != nil {
return false
}
return true
@ -127,5 +131,11 @@ func (m *Model) Expand(items []string) error {
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
}

View File

@ -1,13 +1,51 @@
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
func ConfigureHost(h host.Model) error {
// 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
reloadNginx()
return nil
if err := reloadNginx(); err != 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
import (
"io/fs"
"fmt"
"io/ioutil"
"npm/embed"
"npm/internal/entity/certificate"
"npm/internal/entity/host"
"npm/internal/logger"
"github.com/aymerick/raymond"
)
// WriteTemplate will load, parse and write a template file
func WriteTemplate(templateName, outputFilename string, data map[string]interface{}) error {
// get template file content
subFs, _ := fs.Sub(embed.NginxFiles, "nginx")
template, err := fs.ReadFile(subFs, templateName)
if err != nil {
return err
}
// Render
parsedFile, err := raymond.Render(string(template), data)
if err != nil {
return err
}
// Write it
// nolint: gosec
return ioutil.WriteFile(outputFilename, []byte(parsedFile), 0644)
// TemplateData ...
type TemplateData struct {
ConfDir string
DataDir string
CertsDir string
Host *host.Model
Certificate *certificate.Model
}
func generateHostConfig(template string, data TemplateData) (string, error) {
return raymond.Render(template, data)
}
func writeTemplate(filename, template string, data TemplateData) error {
output, err := generateHostConfig(template, data)
if err != nil {
output = fmt.Sprintf("# Template Error: %s", err.Error())
}
// 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)
}