feat: backend executor implement

This commit is contained in:
2026-08-16 17:12:06 +08:00
parent 390e0565d3
commit 7755da72e7
31 changed files with 2762 additions and 184 deletions
+84 -21
View File
@@ -19,7 +19,11 @@ const (
// DefaultPath is the only default location used by the daemon entrypoint.
DefaultPath = "/etc/yms-daemon/yms-daemon.toml"
BackendTypeNative = "native"
EnvironmentDev = "dev"
EnvironmentProd = "prod"
BackendTypeNative = "native"
BackendTypeContainer = "container"
BackendPort8080 = 8080
BackendPort8081 = 8081
@@ -34,14 +38,32 @@ const (
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"
containerName8080 = "backend-8080"
containerName8081 = "backend-8081"
)
const (
ContainerConfigSource = "/home/yms/conf/yms.yaml"
ContainerConfigTarget = "/app/config/yms.yaml"
ContainerTmpSource = "/home/yms/tmp"
ContainerTmpTarget = "/home/yms/tmp"
ContainerPortEnvironment = "SERVER_PORT"
ContainerConfigEnvironment = "SPRING_CONFIG_LOCATION"
ContainerConfigLocation = "file:/app/config/yms.yaml"
)
// Config is the complete local deployment configuration currently understood by the daemon.
type Config struct {
Daemon Daemon `toml:"daemon"`
Backend Backend `toml:"backend"`
}
// Backend describes the explicitly selected backend runtime and its native blue/green slots.
// Daemon contains machine-wide behavior that is independent of a component runtime.
type Daemon struct {
Environment string `toml:"environment"`
}
// Backend describes the explicitly selected backend runtime and its blue/green slots.
type Backend struct {
Type string `toml:"type"`
ReleaseDir string `toml:"release_dir"`
@@ -50,20 +72,21 @@ type Backend struct {
Slot BackendSlots `toml:"slot"`
}
// BackendSlots lists the only native backend ports supported by the current deployment contract.
// BackendSlots lists the only 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.
// BackendSlot contains exact values for one native or container backend slot.
type BackendSlot struct {
Unit string `toml:"unit"`
JAR string `toml:"jar"`
ContainerName string `toml:"container_name"`
HealthEndpoint string `toml:"health_endpoint"`
}
// Load opens path, performs strict TOML decoding, and validates the native backend contract.
// Load opens path, performs strict TOML decoding, and validates the selected backend contract.
func Load(path string) (Config, error) {
if err := validateAbsolutePath("deployment configuration", path); err != nil {
return Config{}, err
@@ -106,7 +129,14 @@ func validateExactDocumentKeys(document []byte) error {
if err := toml.Unmarshal(document, &root); err != nil {
return err
}
if err := rejectUnknownKeys(root, "", "backend"); err != nil {
if err := rejectUnknownKeys(root, "", "daemon", "backend"); err != nil {
return err
}
daemon, err := exactTable(root, "", "daemon")
if err != nil {
return err
}
if err := rejectUnknownKeys(daemon, "daemon", "environment"); err != nil {
return err
}
backend, err := exactTable(root, "", "backend")
@@ -128,7 +158,7 @@ func validateExactDocumentKeys(document []byte) error {
if err != nil {
return err
}
if err := rejectUnknownKeys(slot, "backend.slot."+port, "unit", "jar", "health_endpoint"); err != nil {
if err := rejectUnknownKeys(slot, "backend.slot."+port, "unit", "jar", "container_name", "health_endpoint"); err != nil {
return err
}
}
@@ -176,23 +206,46 @@ func rejectUnknownKeys(table map[string]any, parent string, allowed ...string) e
// 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)
switch c.Daemon.Environment {
case EnvironmentDev, EnvironmentProd:
default:
return fmt.Errorf("daemon.environment must be %q or %q", EnvironmentDev, EnvironmentProd)
}
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
switch c.Backend.Type {
case BackendTypeNative:
if c.Backend.Slot.Port8080.ContainerName != "" || c.Backend.Slot.Port8081.ContainerName != "" {
return errors.New("native backend slots do not accept container_name")
}
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 := validateNativeSlot("backend.slot.8080", c.Backend.Slot.Port8080, nativeUnit8080, nativeSlotJAR8080, nativeHealthURL8080); err != nil {
return err
}
if err := validateNativeSlot("backend.slot.8081", c.Backend.Slot.Port8081, nativeUnit8081, nativeSlotJAR8081, nativeHealthURL8081); err != nil {
return err
}
case BackendTypeContainer:
if c.Backend.ReleaseDir != "" || c.Backend.ActiveJAR != "" {
return errors.New("container backend does not accept release_dir or active_jar")
}
if c.Backend.Slot.Port8080.Unit != "" || c.Backend.Slot.Port8080.JAR != "" || c.Backend.Slot.Port8081.Unit != "" || c.Backend.Slot.Port8081.JAR != "" {
return errors.New("container backend slots do not accept unit or jar")
}
if err := validateContainerSlot("backend.slot.8080", c.Backend.Slot.Port8080, containerName8080, nativeHealthURL8080); err != nil {
return err
}
if err := validateContainerSlot("backend.slot.8081", c.Backend.Slot.Port8081, containerName8081, nativeHealthURL8081); err != nil {
return err
}
default:
return fmt.Errorf("backend.type must be %q or %q", BackendTypeNative, BackendTypeContainer)
}
return nil
}
@@ -209,7 +262,7 @@ func (b Backend) SlotForPort(port int) (BackendSlot, error) {
}
}
func validateSlot(field string, slot BackendSlot, unit string, jar string, endpoint string) error {
func validateNativeSlot(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)
}
@@ -222,6 +275,16 @@ func validateSlot(field string, slot BackendSlot, unit string, jar string, endpo
return nil
}
func validateContainerSlot(field string, slot BackendSlot, containerName string, endpoint string) error {
if slot.ContainerName != containerName {
return fmt.Errorf("%s.container_name must be %q", field, containerName)
}
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)
+79 -1
View File
@@ -7,7 +7,10 @@ import (
"testing"
)
const validNativeConfig = `[backend]
const validNativeConfig = `[daemon]
environment = "dev"
[backend]
type = "native"
release_dir = "/home/yms/lib/releases"
active_jar = "/home/yms/lib/glory-soft-yms.jar"
@@ -24,6 +27,22 @@ jar = "/home/yms/lib/glory-soft-yms-8081.jar"
health_endpoint = "http://127.0.0.1:8081/yms/actuator/health"
`
const validContainerConfig = `[daemon]
environment = "dev"
[backend]
type = "container"
systemctl_path = "/bin/systemctl"
[backend.slot.8080]
container_name = "backend-8080"
health_endpoint = "http://127.0.0.1:8080/yms/actuator/health"
[backend.slot.8081]
container_name = "backend-8081"
health_endpoint = "http://127.0.0.1:8081/yms/actuator/health"
`
func TestLoadValidNativeConfiguration(t *testing.T) {
path := writeConfig(t, validNativeConfig)
@@ -31,6 +50,9 @@ func TestLoadValidNativeConfiguration(t *testing.T) {
if err != nil {
t.Fatalf("load native deployment configuration: %v", err)
}
if config.Daemon.Environment != EnvironmentDev {
t.Fatalf("unexpected daemon configuration: %+v", config.Daemon)
}
if config.Backend.Type != BackendTypeNative || config.Backend.SystemctlPath != "/bin/systemctl" {
t.Fatalf("unexpected backend configuration: %+v", config.Backend)
}
@@ -50,6 +72,27 @@ func TestLoadValidNativeConfiguration(t *testing.T) {
}
}
func TestLoadValidContainerConfiguration(t *testing.T) {
config, err := Load(writeConfig(t, validContainerConfig))
if err != nil {
t.Fatalf("load container deployment configuration: %v", err)
}
if config.Backend.Type != BackendTypeContainer {
t.Fatalf("unexpected backend type: %q", config.Backend.Type)
}
if config.Backend.Slot.Port8080.ContainerName != "backend-8080" || config.Backend.Slot.Port8081.ContainerName != "backend-8081" {
t.Fatalf("unexpected container slots: %+v", config.Backend.Slot)
}
}
func TestLoadRejectsChangedContainerName(t *testing.T) {
content := strings.Replace(validContainerConfig, `container_name = "backend-8081"`, `container_name = "backend"`, 1)
_, err := Load(writeConfig(t, content))
if err == nil || !strings.Contains(err.Error(), "backend.slot.8081.container_name") {
t.Fatalf("expected exact container name rejection, got %v", err)
}
}
func TestPackagedNativeConfigurationMatchesContract(t *testing.T) {
path, err := filepath.Abs(filepath.Join("..", "..", "packaging", "etc", "yms-daemon", "yms-daemon.toml"))
if err != nil {
@@ -68,6 +111,41 @@ func TestLoadRejectsUnknownField(t *testing.T) {
}
}
func TestLoadAcceptsProductionEnvironment(t *testing.T) {
content := strings.Replace(validNativeConfig, `environment = "dev"`, `environment = "prod"`, 1)
config, err := Load(writeConfig(t, content))
if err != nil {
t.Fatalf("load production environment: %v", err)
}
if config.Daemon.Environment != EnvironmentProd {
t.Fatalf("unexpected daemon environment: %q", config.Daemon.Environment)
}
}
func TestLoadRejectsMissingDaemonTable(t *testing.T) {
content := strings.Replace(validNativeConfig, "[daemon]\nenvironment = \"dev\"\n\n", "", 1)
_, err := Load(writeConfig(t, content))
if err == nil || !strings.Contains(err.Error(), "daemon table is required") {
t.Fatalf("expected missing daemon table rejection, got %v", err)
}
}
func TestLoadRejectsChangedEnvironment(t *testing.T) {
content := strings.Replace(validNativeConfig, `environment = "dev"`, `environment = "development"`, 1)
_, err := Load(writeConfig(t, content))
if err == nil || !strings.Contains(err.Error(), "daemon.environment") {
t.Fatalf("expected exact daemon environment rejection, got %v", err)
}
}
func TestLoadRejectsUnknownDaemonField(t *testing.T) {
content := strings.Replace(validNativeConfig, `environment = "dev"`, "environment = \"dev\"\nEnvironment = \"dev\"", 1)
_, err := Load(writeConfig(t, content))
if err == nil || !strings.Contains(err.Error(), "daemon.Environment") {
t.Fatalf("expected exact daemon field rejection, got %v", err)
}
}
func TestLoadRejectsWrongTableCase(t *testing.T) {
content := strings.Replace(validNativeConfig, "[backend]", "[Backend]", 1)
_, err := Load(writeConfig(t, content))