This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tend/internal/deploy/operator.go
T
gamertan 00d1dd4209 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>
2026-08-16 19:02:08 -04:00

126 lines
4.3 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only
package deploy
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
"gamertan.com/tend/internal/process"
)
type Operator interface {
Restart(context.Context, string) error
Stop(context.Context, string) error
IsActive(context.Context, string) (bool, error)
StartCandidate(context.Context, string, string, string, map[string]string) error
ValidateCaddy(context.Context, string) error
ReloadCaddy(context.Context) error
Probe(context.Context, string, string, string, string) error
ProbeURL(context.Context, string, string) error
}
type SystemOperator struct {
Runner process.Runner
Timeout time.Duration
}
func (o SystemOperator) Restart(ctx context.Context, unit string) error {
_, err := o.Runner.Run(ctx, "/", nil, "systemctl", "restart", unit)
return err
}
func (o SystemOperator) Stop(ctx context.Context, unit string) error {
_, err := o.Runner.Run(ctx, "/", nil, "systemctl", "stop", unit)
return err
}
func (o SystemOperator) IsActive(ctx context.Context, unit string) (bool, error) {
out, err := o.Runner.Run(ctx, "/", nil, "systemctl", "is-active", unit)
if err != nil {
if strings.TrimSpace(string(out)) == "inactive" || strings.TrimSpace(string(out)) == "failed" {
return false, nil
}
return false, err
}
return strings.TrimSpace(string(out)) == "active", nil
}
func (o SystemOperator) StartCandidate(ctx context.Context, unit, binary, environmentFile string, env map[string]string) error {
args := []string{
"--unit", unit, "--collect",
"--property=DynamicUser=yes", "--property=NoNewPrivileges=yes",
"--property=PrivateDevices=yes", "--property=PrivateTmp=yes",
"--property=ProtectClock=yes", "--property=ProtectControlGroups=yes",
"--property=ProtectHome=yes", "--property=ProtectHostname=yes",
"--property=ProtectKernelLogs=yes", "--property=ProtectKernelModules=yes",
"--property=ProtectKernelTunables=yes", "--property=ProtectSystem=strict",
"--property=RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX",
"--property=RestrictNamespaces=yes", "--property=RestrictRealtime=yes",
"--property=RestrictSUIDSGID=yes", "--property=LockPersonality=yes",
"--property=MemoryDenyWriteExecute=yes", "--property=CapabilityBoundingSet=",
"--property=AmbientCapabilities=",
"--property=EnvironmentFile=" + environmentFile,
}
keys := make([]string, 0, len(env))
for key := range env {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
args = append(args, "--setenv", key+"="+env[key])
}
args = append(args, "--", binary)
_, err := o.Runner.Run(ctx, "/", nil, "systemd-run", args...)
return err
}
func (o SystemOperator) ValidateCaddy(ctx context.Context, path string) error {
_, err := o.Runner.Run(ctx, "/", nil, "caddy", "validate", "--config", path, "--adapter", "caddyfile")
return err
}
func (o SystemOperator) ReloadCaddy(ctx context.Context) error {
_, err := o.Runner.Run(ctx, "/", nil, "systemctl", "reload", "caddy.service")
return err
}
func (o SystemOperator) Probe(ctx context.Context, address, host, path, contains string) error {
u := url.URL{Scheme: "http", Host: address, Path: path}
return o.probeRequest(ctx, u.String(), host, contains)
}
func (o SystemOperator) ProbeURL(ctx context.Context, value, contains string) error {
u, err := url.Parse(value)
if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.Fragment != "" {
return errors.New("public probe URL is invalid")
}
return o.probeRequest(ctx, u.String(), "", contains)
}
func (o SystemOperator) probeRequest(ctx context.Context, value, host, contains string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, value, nil)
if err != nil {
return err
}
if host != "" {
req.Host = host
}
client := &http.Client{Timeout: o.Timeout, CheckRedirect: func(*http.Request, []*http.Request) error { return errors.New("redirect refused") }}
response, err := client.Do(req)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("probe returned HTTP %d", response.StatusCode)
}
body, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
if err != nil {
return err
}
if contains != "" && !strings.Contains(string(body), contains) {
return errors.New("probe response omitted required marker")
}
return nil
}