~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
228 lines · 5.7 KB Raw History
package kcpserver

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"syscall"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/hostsvc"
)

type Shape string

const (
	ShapeSingle    Shape = "single"
	ShapeBlueGreen Shape = "blue-green"
	SlotA                = "a"
	SlotB                = "b"
)

const FrontStopTimeout = 10 * time.Second

var SharedFiles = []string{"apiserver.crt", "apiserver.key", "sa.key", ".admin-token-store", "admin.kubeconfig"}

func SlotNames(s Shape) []string {
	if s == ShapeBlueGreen {
		return []string{SlotA, SlotB}
	}
	return []string{SlotA}
}

func Other(slot string) string {
	if slot == SlotA {
		return SlotB
	}
	return SlotA
}

type SlotRecord struct {
	Unit         string    `json:"unit"`
	Root         string    `json:"root"`
	Port         int       `json:"port"`
	PID          int       `json:"pid"`
	InvocationID string    `json:"invocationID"`
	Exec         string    `json:"exec,omitempty"`
	Kine         string    `json:"kine,omitempty"`
	SHA256       string    `json:"sha256,omitempty"`
	Version      string    `json:"version,omitempty"`
	StartedAt    time.Time `json:"startedAt,omitzero"`
	ReadyAt      time.Time `json:"readyAt,omitzero"`
}

type FrontRecord struct {
	Unit    string `json:"unit"`
	Address string `json:"address"`
	Port    int    `json:"port"`
	Control string `json:"control"`
	PID     int    `json:"pid"`
}

func (f FrontRecord) URL() string {
	return fmt.Sprintf("https://127.0.0.1:%d", f.Port)
}

type Slots struct {
	Shape   Shape                 `json:"shape"`
	Active  string                `json:"active"`
	Slots   map[string]SlotRecord `json:"slots"`
	Front   FrontRecord           `json:"front"`
	KineURL string                `json:"kineURL,omitempty"`
	Updated time.Time             `json:"updated"`
}

func ReadSlots(path string) (Slots, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		return Slots{}, err
	}
	var s Slots
	if err := json.Unmarshal(b, &s); err != nil {
		return Slots{}, fmt.Errorf("kcpserver: %s: %w", path, err)
	}
	if s.Slots == nil {
		s.Slots = map[string]SlotRecord{}
	}
	return s, nil
}

func WriteSlots(path string, s Slots) error {
	s.Updated = time.Now().UTC()
	b, err := json.MarshalIndent(s, "", "  ")
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
		return err
	}
	tmp := path + ".tmp"
	f, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
	if err != nil {
		return err
	}
	if _, err := f.Write(append(b, '\n')); err != nil {
		f.Close()
		return err
	}
	if err := f.Sync(); err != nil {
		f.Close()
		return err
	}
	if err := f.Close(); err != nil {
		return err
	}
	return os.Rename(tmp, path)
}

func copyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()
	tmp := dst + ".tmp"
	out, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
	if err != nil {
		return err
	}
	if _, err := io.Copy(out, in); err != nil {
		out.Close()
		return err
	}
	if err := out.Close(); err != nil {
		return err
	}
	return os.Rename(tmp, dst)
}

func missingShared(dir string) []string {
	var missing []string
	for _, f := range SharedFiles {
		if fi, err := os.Stat(filepath.Join(dir, f)); err != nil || fi.Size() == 0 {
			missing = append(missing, f)
		}
	}
	return missing
}

func HasShared(shared string) bool { return len(missingShared(shared)) == 0 }

func SaveShared(root, shared string) error {
	if missing := missingShared(root); len(missing) > 0 {
		return fmt.Errorf("kcpserver: slot root %s lacks %v; the shared material is all five files or none", root, missing)
	}
	if err := os.MkdirAll(shared, 0o700); err != nil {
		return err
	}
	if err := os.Chmod(shared, 0o700); err != nil {
		return err
	}
	for _, f := range SharedFiles {
		if err := copyFile(filepath.Join(root, f), filepath.Join(shared, f)); err != nil {
			return fmt.Errorf("kcpserver: saving %s: %w", f, err)
		}
	}
	return nil
}

func SeedSlot(shared, root string) error {
	if missing := missingShared(shared); len(missing) > 0 {
		return fmt.Errorf("kcpserver: shared material %s lacks %v: a slot started without admin.kubeconfig next to .admin-token-store refuses to start, and one without the rest serves another CA", shared, missing)
	}
	if err := os.MkdirAll(root, 0o700); err != nil {
		return err
	}
	for _, f := range SharedFiles {
		if err := copyFile(filepath.Join(shared, f), filepath.Join(root, f)); err != nil {
			return fmt.Errorf("kcpserver: seeding %s into %s: %w", f, root, err)
		}
	}
	return nil
}

type FrontConfig struct {
	Unit    string
	Slice   string
	Exe     string
	Owner   string
	Address string
	Control string
	State   string
	Log     string
	FDStore bool
}

const FrontMemoryMax = 256 << 20

func FrontSpec(c FrontConfig) (hostsvc.UnitSpec, error) {
	if !filepath.IsAbs(c.Exe) {
		return hostsvc.UnitSpec{}, fmt.Errorf("kcpserver: the front executable must be an absolute path, got %q", c.Exe)
	}
	if c.Owner == "" || c.Address == "" || c.Control == "" || c.State == "" {
		return hostsvc.UnitSpec{}, errors.New("kcpserver: a front needs its owner, address, control socket and state file")
	}
	spec := hostsvc.UnitSpec{
		Name:        c.Unit,
		Description: "L4 front of kcp for " + c.Owner,
		Exec: []string{c.Exe, "internal", "apiserver-front", "--owner", c.Owner, "--kcp-front-address", c.Address,
			"--front-control", c.Control, "--front-state", c.State},
		WorkDir:     filepath.Dir(c.State),
		Slice:       c.Slice,
		Restart:     hostsvc.RestartAlways,
		KillSignal:  syscall.SIGTERM,
		StopTimeout: FrontStopTimeout,
		Limits:      hostsvc.Limits{MemoryMax: FrontMemoryMax},
	}
	if c.FDStore {
		spec.Exec = append(spec.Exec, "--front-fd-store")
		spec.FDStore = 1
	}
	if c.Log != "" {
		spec.Output = hostsvc.Output{StdoutFile: c.Log, StderrFile: c.Log}
	}
	return spec, nil
}