Export the reviewed allowlisted snapshot from private source commit 07c1655921f21ee5e4fc4d85639d199e8867b17d. This records the Docker Compose activation, schema-compatible rollback, and stateful migration resource findings from Observatory Preview 19 dogfooding. AI-Assisted: OpenAI Codex Signed-off-by: Cole Speelman <crspeelman@gmail.com>
157 lines
4.3 KiB
Go
157 lines
4.3 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package state
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"time"
|
|
)
|
|
|
|
const CandidateLeaseSchemaVersion = 1
|
|
|
|
var (
|
|
leaseOperationPattern = regexp.MustCompile(`^[0-9a-f]{32}$`)
|
|
leaseServicePattern = regexp.MustCompile(`^[a-z][a-z0-9-]{1,62}$`)
|
|
)
|
|
|
|
type CandidateLease struct {
|
|
SchemaVersion int `json:"schema_version"`
|
|
Service string `json:"service"`
|
|
OperationID string `json:"operation_id"`
|
|
Release string `json:"release"`
|
|
Unit string `json:"unit"`
|
|
Address string `json:"address"`
|
|
StartedAt string `json:"started_at"`
|
|
}
|
|
|
|
func CandidateLeasePath(statePath string) string { return statePath + ".candidate-lease.json" }
|
|
|
|
func (l CandidateLease) Validate(root, service string) error {
|
|
if l.SchemaVersion != CandidateLeaseSchemaVersion {
|
|
return errors.New("candidate lease schema version is unsupported")
|
|
}
|
|
if !leaseServicePattern.MatchString(service) || l.Service != service {
|
|
return errors.New("candidate lease service does not match configuration")
|
|
}
|
|
if !leaseOperationPattern.MatchString(l.OperationID) {
|
|
return errors.New("candidate lease operation ID is invalid")
|
|
}
|
|
if err := releaseBelow(root, l.Release); err != nil {
|
|
return fmt.Errorf("candidate lease release: %w", err)
|
|
}
|
|
expectedUnit := service + "-tend-candidate-" + l.OperationID[:12] + ".service"
|
|
if l.Unit != expectedUnit {
|
|
return errors.New("candidate lease unit does not match its operation")
|
|
}
|
|
address, err := netip.ParseAddrPort(l.Address)
|
|
if err != nil || !address.Addr().IsLoopback() || address.Port() == 0 {
|
|
return errors.New("candidate lease address is invalid")
|
|
}
|
|
if _, err := time.Parse(time.RFC3339, l.StartedAt); err != nil {
|
|
return errors.New("candidate lease timestamp is invalid")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func LoadCandidateLease(path, root, service string) (CandidateLease, error) {
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return CandidateLease{}, err
|
|
}
|
|
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() > 64<<10 {
|
|
return CandidateLease{}, errors.New("candidate lease must be a bounded regular file")
|
|
}
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return CandidateLease{}, err
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(body))
|
|
decoder.DisallowUnknownFields()
|
|
var lease CandidateLease
|
|
if err := decoder.Decode(&lease); err != nil {
|
|
return CandidateLease{}, fmt.Errorf("decode candidate lease: %w", err)
|
|
}
|
|
var extra any
|
|
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
|
|
return CandidateLease{}, errors.New("candidate lease contains trailing data")
|
|
}
|
|
if err := lease.Validate(root, service); err != nil {
|
|
return CandidateLease{}, err
|
|
}
|
|
return lease, nil
|
|
}
|
|
|
|
func StoreCandidateLease(path, root string, lease CandidateLease) error {
|
|
if err := lease.Validate(root, lease.Service); err != nil {
|
|
return err
|
|
}
|
|
if info, err := os.Lstat(path); err == nil && (!info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0) {
|
|
return errors.New("candidate lease path must be a regular file, not a symlink")
|
|
} else if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
body, err := json.MarshalIndent(lease, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body = append(body, '\n')
|
|
dir := filepath.Dir(path)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
temporary, err := os.CreateTemp(dir, ".tend-candidate-lease-")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := temporary.Name()
|
|
complete := false
|
|
defer func() {
|
|
_ = temporary.Close()
|
|
if !complete {
|
|
_ = os.Remove(name)
|
|
}
|
|
}()
|
|
if err := temporary.Chmod(0o644); err != nil {
|
|
return err
|
|
}
|
|
if _, err := temporary.Write(body); err != nil {
|
|
return err
|
|
}
|
|
if err := temporary.Sync(); err != nil {
|
|
return err
|
|
}
|
|
if err := temporary.Close(); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(name, path); err != nil {
|
|
return err
|
|
}
|
|
complete = true
|
|
return syncDir(dir)
|
|
}
|
|
|
|
func RemoveCandidateLease(path string) error {
|
|
info, err := os.Lstat(path)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
|
return errors.New("candidate lease path must be a regular file, not a symlink")
|
|
}
|
|
if err := os.Remove(path); err != nil {
|
|
return err
|
|
}
|
|
return syncDir(filepath.Dir(path))
|
|
}
|