All checks were successful
Check / check (pull_request) Successful in 3m24s
- Add cpu_limit (REAL) and memory_limit (INTEGER) columns to apps table via migration 007 - Add CPULimit and MemoryLimit fields to App model with full CRUD support - Add resource limits fields to app edit form with human-friendly memory input (e.g. 256m, 1g, 512k) - Pass CPU and memory limits to Docker container creation via NanoCPUs and Memory host config fields - Extract Docker container creation helpers (buildEnvSlice, buildMounts, buildResources) for cleaner code - Add formatMemoryBytes template function for display - Add comprehensive tests for parsing, formatting, model persistence, and container options
131 lines
3.2 KiB
Go
131 lines
3.2 KiB
Go
// Package templates provides HTML template handling.
|
|
package templates
|
|
|
|
import (
|
|
"embed"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"strconv"
|
|
"sync"
|
|
)
|
|
|
|
//go:embed *.html
|
|
var templatesRaw embed.FS
|
|
|
|
// Template cache variables are global to enable efficient template reuse
|
|
// across requests without re-parsing on each call.
|
|
var (
|
|
//nolint:gochecknoglobals // singleton pattern for template cache
|
|
baseTemplate *template.Template
|
|
//nolint:gochecknoglobals // singleton pattern for template cache
|
|
pageTemplates map[string]*template.Template
|
|
//nolint:gochecknoglobals // protects template cache access
|
|
templatesMutex sync.RWMutex
|
|
)
|
|
|
|
// templateFuncs returns the custom template function map.
|
|
func templateFuncs() template.FuncMap {
|
|
return template.FuncMap{
|
|
"formatMemoryBytes": formatMemoryBytes,
|
|
}
|
|
}
|
|
|
|
// Memory unit constants.
|
|
const (
|
|
memGigabyte = 1024 * 1024 * 1024
|
|
memMegabyte = 1024 * 1024
|
|
memKilobyte = 1024
|
|
)
|
|
|
|
// formatMemoryBytes formats bytes into a human-readable string with unit suffix.
|
|
func formatMemoryBytes(bytes int64) string {
|
|
switch {
|
|
case bytes >= memGigabyte && bytes%memGigabyte == 0:
|
|
return strconv.FormatInt(bytes/memGigabyte, 10) + "g"
|
|
case bytes >= memMegabyte && bytes%memMegabyte == 0:
|
|
return strconv.FormatInt(bytes/memMegabyte, 10) + "m"
|
|
case bytes >= memKilobyte && bytes%memKilobyte == 0:
|
|
return strconv.FormatInt(bytes/memKilobyte, 10) + "k"
|
|
default:
|
|
return strconv.FormatInt(bytes, 10)
|
|
}
|
|
}
|
|
|
|
// initTemplates parses base template and creates cloned templates for each page.
|
|
func initTemplates() {
|
|
templatesMutex.Lock()
|
|
defer templatesMutex.Unlock()
|
|
|
|
if pageTemplates != nil {
|
|
return
|
|
}
|
|
|
|
// Parse base template with shared components and custom functions
|
|
baseTemplate = template.Must(
|
|
template.New("base.html").Funcs(templateFuncs()).ParseFS(templatesRaw, "base.html"),
|
|
)
|
|
|
|
// Pages that extend base
|
|
pages := []string{
|
|
"setup.html",
|
|
"login.html",
|
|
"dashboard.html",
|
|
"app_new.html",
|
|
"app_detail.html",
|
|
"app_edit.html",
|
|
"deployments.html",
|
|
"webhook_events.html",
|
|
}
|
|
|
|
pageTemplates = make(map[string]*template.Template)
|
|
|
|
for _, page := range pages {
|
|
// Clone base template and parse page-specific template into it
|
|
clone := template.Must(baseTemplate.Clone())
|
|
pageTemplates[page] = template.Must(clone.ParseFS(templatesRaw, page))
|
|
}
|
|
}
|
|
|
|
// GetParsed returns a template executor that routes to the correct page template.
|
|
func GetParsed() *TemplateExecutor {
|
|
initTemplates()
|
|
|
|
return &TemplateExecutor{}
|
|
}
|
|
|
|
// TemplateExecutor executes templates using the correct cloned template set.
|
|
type TemplateExecutor struct{}
|
|
|
|
// ExecuteTemplate executes the named template with the given data.
|
|
func (t *TemplateExecutor) ExecuteTemplate(
|
|
writer io.Writer,
|
|
name string,
|
|
data any,
|
|
) error {
|
|
templatesMutex.RLock()
|
|
|
|
tmpl, ok := pageTemplates[name]
|
|
|
|
templatesMutex.RUnlock()
|
|
|
|
if !ok {
|
|
// Fallback for non-page templates
|
|
err := baseTemplate.ExecuteTemplate(writer, name, data)
|
|
if err != nil {
|
|
return fmt.Errorf("execute base template %s: %w", name, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Execute the "base" template from the cloned set
|
|
// (which has page-specific overrides)
|
|
err := tmpl.ExecuteTemplate(writer, "base", data)
|
|
if err != nil {
|
|
return fmt.Errorf("execute page template %s: %w", name, err)
|
|
}
|
|
|
|
return nil
|
|
}
|