feat: publish Tend v0.2 preview source
Publish the reviewed allowlisted snapshot whose exact binary completed maintenance deployment, rollback, and reactivation exercises for Gamertan and Sandwich Hime. Private-Source-Commit: 4d7094c8b7c61991bfb67b11fc1558724c874eb2 Private-Source-Tree: 54a2f74804f7acddf3755d7d4da5b97f5fc28381 AI-Assistance: OpenAI Codex assisted implementation, testing, security review, and release verification. Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
+45
-25
@@ -9,13 +9,17 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const SchemaVersion = 1
|
||||
const (
|
||||
SchemaVersion = 2
|
||||
SharedLockFile = "/run/lock/tend-deploy.lock"
|
||||
)
|
||||
|
||||
var (
|
||||
namePattern = regexp.MustCompile(`^[a-z][a-z0-9-]{1,62}$`)
|
||||
@@ -33,8 +37,9 @@ type Config struct {
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
Name string `json:"name"`
|
||||
AllowedHost string `json:"allowed_host"`
|
||||
Name string `json:"name"`
|
||||
AllowedHost string `json:"allowed_host"`
|
||||
EnvironmentFile string `json:"environment_file"`
|
||||
}
|
||||
|
||||
type Build struct {
|
||||
@@ -47,16 +52,17 @@ type Build struct {
|
||||
}
|
||||
|
||||
type Deployment struct {
|
||||
Strategy string `json:"strategy"`
|
||||
Root string `json:"root"`
|
||||
LockFile string `json:"lock_file"`
|
||||
StateFile string `json:"state_file"`
|
||||
HealthPath string `json:"health_path"`
|
||||
ReadinessPath string `json:"readiness_path"`
|
||||
CandidateTimeoutSecs int `json:"candidate_timeout_seconds"`
|
||||
Smoke []Smoke `json:"smoke"`
|
||||
BlueGreen *BlueGreen `json:"blue_green,omitempty"`
|
||||
Singleton *Singleton `json:"singleton,omitempty"`
|
||||
Strategy string `json:"strategy"`
|
||||
Root string `json:"root"`
|
||||
LockFile string `json:"lock_file"`
|
||||
StateFile string `json:"state_file"`
|
||||
HealthPath string `json:"health_path"`
|
||||
ReadinessPath string `json:"readiness_path"`
|
||||
CandidateTimeoutSecs int `json:"candidate_timeout_seconds"`
|
||||
Smoke []Smoke `json:"smoke"`
|
||||
PublicSmoke []PublicSmoke `json:"public_smoke"`
|
||||
BlueGreen *BlueGreen `json:"blue_green,omitempty"`
|
||||
Singleton *Singleton `json:"singleton,omitempty"`
|
||||
}
|
||||
|
||||
type Smoke struct {
|
||||
@@ -64,6 +70,11 @@ type Smoke struct {
|
||||
Contains string `json:"contains"`
|
||||
}
|
||||
|
||||
type PublicSmoke struct {
|
||||
URL string `json:"url"`
|
||||
Contains string `json:"contains"`
|
||||
}
|
||||
|
||||
type BlueGreen struct {
|
||||
CaddyConfig string `json:"caddy_config"`
|
||||
CaddyHandler string `json:"caddy_handler"`
|
||||
@@ -80,13 +91,12 @@ type Slot struct {
|
||||
}
|
||||
|
||||
type Singleton struct {
|
||||
Unit string `json:"unit"`
|
||||
Address string `json:"address"`
|
||||
CandidateAddress string `json:"candidate_address"`
|
||||
ListenEnv string `json:"listen_env"`
|
||||
Environment map[string]string `json:"environment,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func Load(path string) (Config, error) {
|
||||
@@ -140,6 +150,12 @@ func (c Config) Validate() error {
|
||||
if c.Service.AllowedHost == "" || strings.ContainsAny(c.Service.AllowedHost, "/\\\x00\r\n\t ") {
|
||||
return errors.New("service.allowed_host is invalid")
|
||||
}
|
||||
if err := safeAbsolute("service.environment_file", c.Service.EnvironmentFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if !within("/etc/tend/environment", c.Service.EnvironmentFile) {
|
||||
return errors.New("service.environment_file must be below /etc/tend/environment")
|
||||
}
|
||||
if !packagePattern.MatchString(c.Build.Package) || strings.Contains(c.Build.Package, "..") {
|
||||
return errors.New("build.package must be a local package without traversal")
|
||||
}
|
||||
@@ -185,6 +201,15 @@ func (c Config) Validate() error {
|
||||
return fmt.Errorf("deployment.smoke[%d] is invalid", i)
|
||||
}
|
||||
}
|
||||
if len(d.PublicSmoke) == 0 || len(d.PublicSmoke) > 16 {
|
||||
return errors.New("deployment.public_smoke must contain 1 to 16 checks")
|
||||
}
|
||||
for i, smoke := range d.PublicSmoke {
|
||||
parsed, err := url.Parse(smoke.URL)
|
||||
if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.Fragment != "" || parsed.RawQuery != "" || parsed.Opaque != "" || smoke.Contains == "" || len(smoke.Contains) > 4096 || strings.ContainsRune(smoke.Contains, '\x00') {
|
||||
return fmt.Errorf("deployment.public_smoke[%d] is invalid", i)
|
||||
}
|
||||
}
|
||||
switch d.Strategy {
|
||||
case "blue_green":
|
||||
if d.BlueGreen == nil || d.Singleton != nil {
|
||||
@@ -273,11 +298,6 @@ func validateSingleton(root string, s Singleton) error {
|
||||
if s.CurrentLink == s.PreviousLink {
|
||||
return errors.New("current and previous links must differ")
|
||||
}
|
||||
for key, value := range s.Environment {
|
||||
if !regexp.MustCompile(`^[A-Z][A-Z0-9_]{0,63}$`).MatchString(key) || strings.ContainsAny(value, "\x00\r\n") {
|
||||
return fmt.Errorf("singleton environment entry %q is invalid", key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user