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>
126 lines
4.3 KiB
Go
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
|
|
}
|