feat: backend native executor implement
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
// Package deploymentconfig loads the daemon's explicit local deployment configuration.
|
||||
// It never infers deployment type, executable paths, unit names, slot paths, or endpoints.
|
||||
package deploymentconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultPath is the only default location used by the daemon entrypoint.
|
||||
DefaultPath = "/etc/yms-daemon/yms-daemon.toml"
|
||||
|
||||
BackendTypeNative = "native"
|
||||
|
||||
BackendPort8080 = 8080
|
||||
BackendPort8081 = 8081
|
||||
)
|
||||
|
||||
const (
|
||||
nativeReleaseDir = "/home/yms/lib/releases"
|
||||
nativeActiveJAR = "/home/yms/lib/glory-soft-yms.jar"
|
||||
nativeUnit8080 = "yms-backend@8080.service"
|
||||
nativeUnit8081 = "yms-backend@8081.service"
|
||||
nativeSlotJAR8080 = "/home/yms/lib/glory-soft-yms-8080.jar"
|
||||
nativeSlotJAR8081 = "/home/yms/lib/glory-soft-yms-8081.jar"
|
||||
nativeHealthURL8080 = "http://127.0.0.1:8080/yms/actuator/health"
|
||||
nativeHealthURL8081 = "http://127.0.0.1:8081/yms/actuator/health"
|
||||
)
|
||||
|
||||
// Config is the complete local deployment configuration currently understood by the daemon.
|
||||
type Config struct {
|
||||
Backend Backend `toml:"backend"`
|
||||
}
|
||||
|
||||
// Backend describes the explicitly selected backend runtime and its native blue/green slots.
|
||||
type Backend struct {
|
||||
Type string `toml:"type"`
|
||||
ReleaseDir string `toml:"release_dir"`
|
||||
ActiveJAR string `toml:"active_jar"`
|
||||
SystemctlPath string `toml:"systemctl_path"`
|
||||
Slot BackendSlots `toml:"slot"`
|
||||
}
|
||||
|
||||
// BackendSlots lists the only native backend ports supported by the current deployment contract.
|
||||
type BackendSlots struct {
|
||||
Port8080 BackendSlot `toml:"8080"`
|
||||
Port8081 BackendSlot `toml:"8081"`
|
||||
}
|
||||
|
||||
// BackendSlot contains values that are passed to the native backend executor without derivation.
|
||||
type BackendSlot struct {
|
||||
Unit string `toml:"unit"`
|
||||
JAR string `toml:"jar"`
|
||||
HealthEndpoint string `toml:"health_endpoint"`
|
||||
}
|
||||
|
||||
// Load opens path, performs strict TOML decoding, and validates the native backend contract.
|
||||
func Load(path string) (Config, error) {
|
||||
if err := validateAbsolutePath("deployment configuration", path); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("open deployment configuration %s: %w", path, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("inspect deployment configuration %s: %w", path, err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return Config{}, fmt.Errorf("deployment configuration is not a regular file: %s", path)
|
||||
}
|
||||
|
||||
document, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("read deployment configuration %s: %w", path, err)
|
||||
}
|
||||
if err := validateExactDocumentKeys(document); err != nil {
|
||||
return Config{}, fmt.Errorf("decode deployment configuration %s: %w", path, err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := toml.NewDecoder(bytes.NewReader(document)).DisallowUnknownFields().Decode(&config); err != nil {
|
||||
return Config{}, fmt.Errorf("decode deployment configuration %s: %w", path, err)
|
||||
}
|
||||
if err := config.Validate(); err != nil {
|
||||
return Config{}, fmt.Errorf("validate deployment configuration %s: %w", path, err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func validateExactDocumentKeys(document []byte) error {
|
||||
var root map[string]any
|
||||
if err := toml.Unmarshal(document, &root); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rejectUnknownKeys(root, "", "backend"); err != nil {
|
||||
return err
|
||||
}
|
||||
backend, err := exactTable(root, "", "backend")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rejectUnknownKeys(backend, "backend", "type", "release_dir", "active_jar", "systemctl_path", "slot"); err != nil {
|
||||
return err
|
||||
}
|
||||
slots, err := exactTable(backend, "backend", "slot")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rejectUnknownKeys(slots, "backend.slot", "8080", "8081"); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, port := range []string{"8080", "8081"} {
|
||||
slot, err := exactTable(slots, "backend.slot", port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rejectUnknownKeys(slot, "backend.slot."+port, "unit", "jar", "health_endpoint"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func exactTable(table map[string]any, parent string, key string) (map[string]any, error) {
|
||||
value, found := table[key]
|
||||
field := key
|
||||
if parent != "" {
|
||||
field = parent + "." + key
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%s table is required", field)
|
||||
}
|
||||
nested, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s must be a table", field)
|
||||
}
|
||||
return nested, nil
|
||||
}
|
||||
|
||||
func rejectUnknownKeys(table map[string]any, parent string, allowed ...string) error {
|
||||
known := make(map[string]struct{}, len(allowed))
|
||||
for _, key := range allowed {
|
||||
known[key] = struct{}{}
|
||||
}
|
||||
keys := make([]string, 0, len(table))
|
||||
for key := range table {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
if _, found := known[key]; found {
|
||||
continue
|
||||
}
|
||||
field := key
|
||||
if parent != "" {
|
||||
field = parent + "." + key
|
||||
}
|
||||
return fmt.Errorf("unknown deployment configuration field %s", field)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate rejects incomplete or altered local deployment identifiers.
|
||||
func (c Config) Validate() error {
|
||||
if c.Backend.Type != BackendTypeNative {
|
||||
return fmt.Errorf("backend.type must be %q", BackendTypeNative)
|
||||
}
|
||||
if c.Backend.ReleaseDir != nativeReleaseDir {
|
||||
return fmt.Errorf("backend.release_dir must be %q", nativeReleaseDir)
|
||||
}
|
||||
if c.Backend.ActiveJAR != nativeActiveJAR {
|
||||
return fmt.Errorf("backend.active_jar must be %q", nativeActiveJAR)
|
||||
}
|
||||
if err := validateAbsolutePath("backend.systemctl_path", c.Backend.SystemctlPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateSlot("backend.slot.8080", c.Backend.Slot.Port8080, nativeUnit8080, nativeSlotJAR8080, nativeHealthURL8080); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateSlot("backend.slot.8081", c.Backend.Slot.Port8081, nativeUnit8081, nativeSlotJAR8081, nativeHealthURL8081); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Slot returns the exact configuration for one supported backend port.
|
||||
func (b Backend) SlotForPort(port int) (BackendSlot, error) {
|
||||
switch port {
|
||||
case BackendPort8080:
|
||||
return b.Slot.Port8080, nil
|
||||
case BackendPort8081:
|
||||
return b.Slot.Port8081, nil
|
||||
default:
|
||||
return BackendSlot{}, fmt.Errorf("unsupported native backend port: %d", port)
|
||||
}
|
||||
}
|
||||
|
||||
func validateSlot(field string, slot BackendSlot, unit string, jar string, endpoint string) error {
|
||||
if slot.Unit != unit {
|
||||
return fmt.Errorf("%s.unit must be %q", field, unit)
|
||||
}
|
||||
if slot.JAR != jar {
|
||||
return fmt.Errorf("%s.jar must be %q", field, jar)
|
||||
}
|
||||
if slot.HealthEndpoint != endpoint {
|
||||
return fmt.Errorf("%s.health_endpoint must be %q", field, endpoint)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAbsolutePath(field string, value string) error {
|
||||
if value == "" {
|
||||
return fmt.Errorf("%s is required", field)
|
||||
}
|
||||
if strings.TrimSpace(value) != value {
|
||||
return fmt.Errorf("%s must not contain surrounding whitespace", field)
|
||||
}
|
||||
if !filepath.IsAbs(value) {
|
||||
return fmt.Errorf("%s must be an absolute path", field)
|
||||
}
|
||||
if filepath.Clean(value) != value {
|
||||
return fmt.Errorf("%s must be a clean absolute path", field)
|
||||
}
|
||||
if strings.ContainsRune(value, '\x00') {
|
||||
return errors.New(field + " contains a NUL byte")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user