feat: publish Tend v0.2 Preview 2 source
Export the reviewed allowlisted snapshot from private source commit 8aab3db43f35e6a49aa497f45d73701b13fc9f32 and tree 992132ea4703437dc13ffdbb04a077816c02caf9. This includes routed singleton continuity, deployment evidence, strict schema-2 configuration, restricted transport, and the independently compilable public-tree guard. AI-Assisted: OpenAI Codex Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
@@ -56,9 +56,11 @@ type Deployment struct {
|
||||
Root string `json:"root"`
|
||||
LockFile string `json:"lock_file"`
|
||||
StateFile string `json:"state_file"`
|
||||
EventLog string `json:"event_log"`
|
||||
HealthPath string `json:"health_path"`
|
||||
ReadinessPath string `json:"readiness_path"`
|
||||
CandidateTimeoutSecs int `json:"candidate_timeout_seconds"`
|
||||
ActivationWindowSecs int `json:"activation_window_seconds"`
|
||||
Smoke []Smoke `json:"smoke"`
|
||||
PublicSmoke []PublicSmoke `json:"public_smoke"`
|
||||
BlueGreen *BlueGreen `json:"blue_green,omitempty"`
|
||||
@@ -91,12 +93,15 @@ type Slot struct {
|
||||
}
|
||||
|
||||
type Singleton struct {
|
||||
Unit string `json:"unit"`
|
||||
Address string `json:"address"`
|
||||
CandidateAddress string `json:"candidate_address"`
|
||||
ListenEnv string `json:"listen_env"`
|
||||
CurrentLink string `json:"current_link"`
|
||||
PreviousLink string `json:"previous_link"`
|
||||
Unit string `json:"unit"`
|
||||
Address string `json:"address"`
|
||||
CandidateAddress string `json:"candidate_address"`
|
||||
ListenEnv string `json:"listen_env"`
|
||||
CurrentLink string `json:"current_link"`
|
||||
PreviousLink string `json:"previous_link"`
|
||||
CaddyConfig string `json:"caddy_config"`
|
||||
CaddyHandler string `json:"caddy_handler"`
|
||||
CaddyHandlerTemplate string `json:"caddy_handler_template"`
|
||||
}
|
||||
|
||||
func Load(path string) (Config, error) {
|
||||
@@ -187,12 +192,21 @@ func (c Config) Validate() error {
|
||||
if filepath.Clean(d.StateFile) == filepath.Clean(d.Root) || !within(d.Root, d.StateFile) {
|
||||
return errors.New("deployment.state_file must be below deployment.root")
|
||||
}
|
||||
if err := safeAbsolute("deployment.event_log", d.EventLog); err != nil {
|
||||
return err
|
||||
}
|
||||
if filepath.Clean(d.EventLog) == filepath.Clean(d.Root) || !within(d.Root, d.EventLog) || filepath.Clean(d.EventLog) == filepath.Clean(d.StateFile) {
|
||||
return errors.New("deployment.event_log must be a distinct file below deployment.root")
|
||||
}
|
||||
if !safeHTTPPath(d.HealthPath) || !safeHTTPPath(d.ReadinessPath) {
|
||||
return errors.New("health and readiness paths must be absolute HTTP paths")
|
||||
}
|
||||
if d.CandidateTimeoutSecs < 2 || d.CandidateTimeoutSecs > 300 {
|
||||
return errors.New("candidate_timeout_seconds must be between 2 and 300")
|
||||
}
|
||||
if d.ActivationWindowSecs < 1 || d.ActivationWindowSecs > 120 {
|
||||
return errors.New("activation_window_seconds must be between 1 and 120")
|
||||
}
|
||||
if len(d.Smoke) == 0 || len(d.Smoke) > 32 {
|
||||
return errors.New("deployment.smoke must contain 1 to 32 checks")
|
||||
}
|
||||
@@ -298,6 +312,14 @@ func validateSingleton(root string, s Singleton) error {
|
||||
if s.CurrentLink == s.PreviousLink {
|
||||
return errors.New("current and previous links must differ")
|
||||
}
|
||||
for label, path := range map[string]string{"caddy_config": s.CaddyConfig, "caddy_handler": s.CaddyHandler, "caddy_handler_template": s.CaddyHandlerTemplate} {
|
||||
if err := safeAbsolute("deployment.singleton."+label, path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if filepath.Clean(s.CaddyHandler) == filepath.Clean(s.CaddyHandlerTemplate) {
|
||||
return errors.New("singleton Caddy handler and template must be different files")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ func validConfig() Config {
|
||||
Build: Build{Package: "./cmd/site", Binary: "example-site", Branch: "main"},
|
||||
Deployment: Deployment{
|
||||
Strategy: "blue_green", Root: "/opt/example-site", LockFile: SharedLockFile,
|
||||
StateFile: "/opt/example-site/state.json", HealthPath: "/healthz", ReadinessPath: "/readyz",
|
||||
CandidateTimeoutSecs: 30, Smoke: []Smoke{{Path: "/", Contains: "Example"}},
|
||||
StateFile: "/opt/example-site/state.json", EventLog: "/opt/example-site/deployment-events.jsonl", HealthPath: "/healthz", ReadinessPath: "/readyz",
|
||||
CandidateTimeoutSecs: 30, ActivationWindowSecs: 10, Smoke: []Smoke{{Path: "/", Contains: "Example"}},
|
||||
PublicSmoke: []PublicSmoke{{URL: "https://example.test/", Contains: "Example"}},
|
||||
BlueGreen: &BlueGreen{
|
||||
CaddyConfig: "/etc/caddy/Caddyfile", CaddyHandler: "/etc/caddy/example.caddy",
|
||||
@@ -34,6 +34,24 @@ func TestValidateAcceptsBlueGreen(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSingletonRequiresDistinctCaddyHandoffFiles(t *testing.T) {
|
||||
cfg := validConfig()
|
||||
cfg.Deployment.Strategy = "singleton_candidate"
|
||||
cfg.Deployment.BlueGreen = nil
|
||||
cfg.Deployment.Singleton = &Singleton{
|
||||
Unit: "example-site.service", Address: "127.0.0.1:8092", CandidateAddress: "127.0.0.1:18092", ListenEnv: "EXAMPLE_LISTEN",
|
||||
CurrentLink: "/opt/example-site/current", PreviousLink: "/opt/example-site/previous", CaddyConfig: "/etc/caddy/Caddyfile",
|
||||
CaddyHandler: "/etc/caddy/example-site.caddy", CaddyHandlerTemplate: "/etc/tend/caddy/example-site.template",
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg.Deployment.Singleton.CaddyHandlerTemplate = cfg.Deployment.Singleton.CaddyHandler
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("expected shared handler/template path to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsHostileValues(t *testing.T) {
|
||||
tests := map[string]func(*Config){
|
||||
"unknown strategy": func(c *Config) { c.Deployment.Strategy = "shell" },
|
||||
|
||||
Reference in New Issue
Block a user