repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/toad/internal/enroll/enroll.go
Rawpackage enroll
import (
"encoding/json"
"fmt"
"net/url"
"os"
"regexp"
"strings"
"time"
)
// Service is one operator-enrolled workload.
// Every field is infrastructure-owned; a deployment request can change none of them.
type Service struct {
Name string `json:"name"`
Unit string `json:"unit"`
ImageRepository string `json:"imageRepository"`
UnitTemplateFile string `json:"unitTemplateFile"`
ContainerName string `json:"containerName"`
ReadinessURL string `json:"readinessURL"`
Probation Probation `json:"probation"`
unitTemplate string
}
type Probation struct {
Deadline string `json:"deadline"`
StableFor string `json:"stableFor"`
RestartBudget int `json:"restartBudget"`
deadline time.Duration
stableFor time.Duration
}
func (p Probation) DeadlineDuration() time.Duration { return p.deadline }
func (p Probation) StableForDuration() time.Duration { return p.stableFor }
var segment = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,63}$`)
type Set struct {
services map[string]Service
}
func Load(path string) (Set, error) {
data, err := os.ReadFile(path)
if err != nil {
return Set{}, err
}
var list []Service
decoder := json.NewDecoder(strings.NewReader(string(data)))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&list); err != nil {
return Set{}, fmt.Errorf("enrollment %s: %w", path, err)
}
set := Set{services: make(map[string]Service, len(list))}
for _, service := range list {
prepared, err := prepare(service)
if err != nil {
return Set{}, fmt.Errorf("enrollment %s: %w", path, err)
}
if _, exists := set.services[prepared.Name]; exists {
return Set{}, fmt.Errorf("enrollment %s: duplicate service %q", path, prepared.Name)
}
set.services[prepared.Name] = prepared
}
return set, nil
}
func (s Set) Lookup(name string) (Service, bool) {
service, ok := s.services[name]
return service, ok
}
func (s Set) Names() []string {
names := make([]string, 0, len(s.services))
for name := range s.services {
names = append(names, name)
}
return names
}
// UnitFileName is the Quadlet source file installed for this service.
func (s Service) UnitFileName() string { return s.Unit + ".container" }
// UnitName is the generated systemd unit controlled during a rollout.
func (s Service) UnitName() string { return s.Unit + ".service" }
// RenderUnit produces the unit content for one pinned image reference.
func (s Service) RenderUnit(reference string) string {
return strings.ReplaceAll(s.unitTemplate, "${image}", reference)
}
// Reference builds the only image reference this service may run for a digest.
func (s Service) Reference(digest string) string {
return s.ImageRepository + "@" + digest
}
var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
func ValidDigest(digest string) bool { return digestPattern.MatchString(digest) }
func prepare(service Service) (Service, error) {
if !segment.MatchString(service.Name) {
return Service{}, fmt.Errorf("invalid service name %q", service.Name)
}
if !segment.MatchString(service.Unit) {
return Service{}, fmt.Errorf("service %s: invalid unit %q", service.Name, service.Unit)
}
if !segment.MatchString(service.ContainerName) {
return Service{}, fmt.Errorf("service %s: invalid container name %q", service.Name, service.ContainerName)
}
if strings.ContainsAny(service.ImageRepository, " \t@:") || !strings.Contains(service.ImageRepository, "/") {
return Service{}, fmt.Errorf("service %s: invalid image repository %q", service.Name, service.ImageRepository)
}
probe, err := url.Parse(service.ReadinessURL)
if err != nil || (probe.Scheme != "http" && probe.Scheme != "https") || probe.Host == "" {
return Service{}, fmt.Errorf("service %s: invalid readiness URL %q", service.Name, service.ReadinessURL)
}
if service.UnitTemplateFile == "" {
return Service{}, fmt.Errorf("service %s: unit template file is required", service.Name)
}
template, err := os.ReadFile(service.UnitTemplateFile)
if err != nil {
return Service{}, fmt.Errorf("service %s: %w", service.Name, err)
}
service.unitTemplate = string(template)
if !strings.Contains(service.unitTemplate, "${image}") {
return Service{}, fmt.Errorf("service %s: unit template has no ${image} placeholder", service.Name)
}
if strings.Contains(service.unitTemplate, "AutoUpdate") {
return Service{}, fmt.Errorf("service %s: enrolled unit must not use AutoUpdate", service.Name)
}
if service.Probation.deadline, err = time.ParseDuration(service.Probation.Deadline); err != nil || service.Probation.deadline <= 0 {
return Service{}, fmt.Errorf("service %s: invalid probation deadline %q", service.Name, service.Probation.Deadline)
}
if service.Probation.stableFor, err = time.ParseDuration(service.Probation.StableFor); err != nil || service.Probation.stableFor <= 0 {
return Service{}, fmt.Errorf("service %s: invalid probation stableFor %q", service.Name, service.Probation.StableFor)
}
if service.Probation.stableFor >= service.Probation.deadline {
return Service{}, fmt.Errorf("service %s: probation deadline must exceed stableFor", service.Name)
}
if service.Probation.RestartBudget < 0 {
return Service{}, fmt.Errorf("service %s: negative restart budget", service.Name)
}
return service, nil
}