// 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 }