feat: backend native executor implement

This commit is contained in:
2026-08-16 01:27:30 +08:00
parent fbce91d797
commit 390e0565d3
44 changed files with 6430 additions and 31 deletions
+242
View File
@@ -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
}
+159
View File
@@ -0,0 +1,159 @@
package deploymentconfig
import (
"os"
"path/filepath"
"strings"
"testing"
)
const validNativeConfig = `[backend]
type = "native"
release_dir = "/home/yms/lib/releases"
active_jar = "/home/yms/lib/glory-soft-yms.jar"
systemctl_path = "/bin/systemctl"
[backend.slot.8080]
unit = "yms-backend@8080.service"
jar = "/home/yms/lib/glory-soft-yms-8080.jar"
health_endpoint = "http://127.0.0.1:8080/yms/actuator/health"
[backend.slot.8081]
unit = "yms-backend@8081.service"
jar = "/home/yms/lib/glory-soft-yms-8081.jar"
health_endpoint = "http://127.0.0.1:8081/yms/actuator/health"
`
func TestLoadValidNativeConfiguration(t *testing.T) {
path := writeConfig(t, validNativeConfig)
config, err := Load(path)
if err != nil {
t.Fatalf("load native deployment configuration: %v", err)
}
if config.Backend.Type != BackendTypeNative || config.Backend.SystemctlPath != "/bin/systemctl" {
t.Fatalf("unexpected backend configuration: %+v", config.Backend)
}
slot8080, err := config.Backend.SlotForPort(BackendPort8080)
if err != nil {
t.Fatalf("read 8080 slot: %v", err)
}
if slot8080.Unit != nativeUnit8080 || slot8080.JAR != nativeSlotJAR8080 || slot8080.HealthEndpoint != nativeHealthURL8080 {
t.Fatalf("unexpected 8080 slot: %+v", slot8080)
}
slot8081, err := config.Backend.SlotForPort(BackendPort8081)
if err != nil {
t.Fatalf("read 8081 slot: %v", err)
}
if slot8081.Unit != nativeUnit8081 || slot8081.JAR != nativeSlotJAR8081 || slot8081.HealthEndpoint != nativeHealthURL8081 {
t.Fatalf("unexpected 8081 slot: %+v", slot8081)
}
}
func TestPackagedNativeConfigurationMatchesContract(t *testing.T) {
path, err := filepath.Abs(filepath.Join("..", "..", "packaging", "etc", "yms-daemon", "yms-daemon.toml"))
if err != nil {
t.Fatalf("resolve packaged native deployment configuration: %v", err)
}
if _, err := Load(path); err != nil {
t.Fatalf("load packaged native deployment configuration: %v", err)
}
}
func TestLoadRejectsUnknownField(t *testing.T) {
content := strings.Replace(validNativeConfig, "systemctl_path = \"/bin/systemctl\"", "systemctl_path = \"/bin/systemctl\"\nSystemctlPath = \"/usr/bin/systemctl\"", 1)
_, err := Load(writeConfig(t, content))
if err == nil || !strings.Contains(err.Error(), "SystemctlPath") {
t.Fatalf("expected exact unknown field rejection, got %v", err)
}
}
func TestLoadRejectsWrongTableCase(t *testing.T) {
content := strings.Replace(validNativeConfig, "[backend]", "[Backend]", 1)
_, err := Load(writeConfig(t, content))
if err == nil || !strings.Contains(err.Error(), "Backend") {
t.Fatalf("expected exact table case rejection, got %v", err)
}
}
func TestLoadRejectsMissingNativeField(t *testing.T) {
content := strings.Replace(validNativeConfig, "active_jar = \"/home/yms/lib/glory-soft-yms.jar\"\n", "", 1)
_, err := Load(writeConfig(t, content))
if err == nil || !strings.Contains(err.Error(), "backend.active_jar") {
t.Fatalf("expected missing active JAR rejection, got %v", err)
}
}
func TestLoadRejectsUnknownSlot(t *testing.T) {
content := validNativeConfig + `
[backend.slot.9090]
unit = "yms-backend@9090.service"
jar = "/home/yms/lib/glory-soft-yms-9090.jar"
health_endpoint = "http://127.0.0.1:9090/yms/actuator/health"
`
_, err := Load(writeConfig(t, content))
if err == nil || !strings.Contains(err.Error(), "9090") {
t.Fatalf("expected unknown slot rejection, got %v", err)
}
}
func TestLoadRejectsChangedSlotEndpoint(t *testing.T) {
content := strings.Replace(validNativeConfig, nativeHealthURL8080, nativeHealthURL8081, 1)
_, err := Load(writeConfig(t, content))
if err == nil || !strings.Contains(err.Error(), "backend.slot.8080.health_endpoint") {
t.Fatalf("expected changed health endpoint rejection, got %v", err)
}
}
func TestLoadAcceptsExplicitUsrBinSystemctlPath(t *testing.T) {
content := strings.Replace(validNativeConfig, "/bin/systemctl", "/usr/bin/systemctl", 1)
config, err := Load(writeConfig(t, content))
if err != nil {
t.Fatalf("load explicit /usr/bin/systemctl path: %v", err)
}
if config.Backend.SystemctlPath != "/usr/bin/systemctl" {
t.Fatalf("unexpected systemctl path: %q", config.Backend.SystemctlPath)
}
}
func TestLoadRejectsChangedBackendTypeCase(t *testing.T) {
content := strings.Replace(validNativeConfig, `type = "native"`, `type = "Native"`, 1)
_, err := Load(writeConfig(t, content))
if err == nil || !strings.Contains(err.Error(), "backend.type") {
t.Fatalf("expected exact backend type rejection, got %v", err)
}
}
func TestLoadRejectsDuplicateField(t *testing.T) {
content := strings.Replace(validNativeConfig, "type = \"native\"", "type = \"native\"\ntype = \"native\"", 1)
_, err := Load(writeConfig(t, content))
if err == nil {
t.Fatal("expected duplicate field rejection")
}
}
func TestLoadRequiresAbsoluteConfigurationPath(t *testing.T) {
_, err := Load("yms-daemon.toml")
if err == nil || !strings.Contains(err.Error(), "absolute path") {
t.Fatalf("expected absolute configuration path rejection, got %v", err)
}
}
func TestSlotForPortRejectsUnsupportedPort(t *testing.T) {
config, err := Load(writeConfig(t, validNativeConfig))
if err != nil {
t.Fatalf("load native deployment configuration: %v", err)
}
if _, err := config.Backend.SlotForPort(9090); err == nil {
t.Fatal("expected unsupported port rejection")
}
}
func writeConfig(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "yms-daemon.toml")
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("write deployment configuration: %v", err)
}
return path
}