~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
276 lines · 9.3 KB Raw History
package bootstrap

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"syscall"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/bootstrap/table"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/devinstance"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/hostsvc"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kcpdatastore"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kcpserver"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/opt"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/pgunit"
)

const (
	ProfileQuick         = "quick"
	ProfileProdlike      = "prodlike"
	ProfileMarketNetwork = "market-network"
	CoreStopTimeout      = 10 * time.Second
	CoreName             = "socialweb-computer"
	DropInName           = "socialweb-bootstrap.conf"
	InstallFrontAddr     = "127.0.0.1:0"
	PendingFrontURL      = "https://front.pending.invalid"
	RequestersDir        = "requesters"
	RequesterInstanceSub = "dev"
	RequesterReadyFile   = "ready.json"
	RequesterRecordFile  = "requester.json"
)

type Params struct {
	App          table.App
	Dir          string
	Run          string
	Logs         string
	Exe          string
	KcpPath      string
	KinePath     string
	PostgresRoot string
	FrontAddress string
	FDStore      bool
	Owner        string
	InstanceDir  string
	ReadyTimeout time.Duration
}

type InstalledUnit struct {
	Entry table.Entry
	Unit  string
	Path  string
	Exec  string
	File  hostsvc.InstalledFile
}

func DevApps(profile string, l devinstance.Layout) ([]table.App, error) {
	switch profile {
	case ProfileQuick, ProfileMarketNetwork:
		return []table.App{table.Dev(l.Short, table.StoreSqlite)}, nil
	case ProfileProdlike:
		return []table.App{table.Dev(l.Short, table.StorePostgres)}, nil
	}
	return nil, fmt.Errorf("bootstrap: unknown profile %q: want quick, prodlike or market-network", profile)
}

func (c Config) Params(a table.App) Params {
	l := c.Layout
	p := Params{App: a, Exe: c.Exe, KcpPath: c.KcpPath, KinePath: c.KinePath, PostgresRoot: c.PostgresRoot,
		FrontAddress: c.frontAddress(), FDStore: c.fdStore, Owner: l.ID, InstanceDir: l.StateDir, ReadyTimeout: c.ReadyTimeout,
		Dir: l.KcpDir(), Run: l.RuntimeDir, Logs: l.Logs}
	if a.Key != l.Short {
		p.Dir = filepath.Join(l.StateDir, "apps", a.Key, "kcp")
		p.Run = filepath.Join(l.RuntimeDir, a.Key)
		p.Logs = filepath.Join(l.StateDir, "apps", a.Key, "logs")
		p.Owner = l.ID + "/" + a.Key
		p.InstanceDir = filepath.Join(l.StateDir, "apps", a.Key)
	}
	return p
}

func RequesterInstanceDir(state, name string) string {
	return filepath.Join(state, RequestersDir, name, RequesterInstanceSub, name)
}

func AppDir(a table.App, state string) string {
	switch a.Role {
	case table.RoleBidder:
		return filepath.Join(state, "handles", a.Key, "kcp")
	case table.RoleRequester:
		return filepath.Join(RequesterInstanceDir(state, a.Name), "kcp")
	}
	return filepath.Join(state, "kcp")
}

func InstallParams(a table.App, state string) Params {
	dir := AppDir(a, state)
	bin := filepath.Join(dir, "bin")
	return Params{App: a, Dir: dir, Run: filepath.Join(dir, "run"), Exe: filepath.Join(state, "bin", "current", CoreName),
		KcpPath: filepath.Join(bin, "kcp"), KinePath: filepath.Join(bin, "kine"), PostgresRoot: filepath.Join(bin, pgunit.Asset),
		FrontAddress: InstallFrontAddr, Owner: a.Key, InstanceDir: state}
}

func (p Params) KineSocket(slot string) string { return filepath.Join(p.Run, "kine-"+slot+".sock") }

func (p Params) KineURL(slot string) string { return "unix://" + p.KineSocket(slot) }

func (p Params) KineDir(slot string) string { return filepath.Join(p.Dir, "kine-"+slot) }

func (p Params) PgSocketDir() string { return filepath.Join(p.Run, "pg") }

func (p Params) PgData() string { return filepath.Join(p.Dir, "pg", "data") }

func (p Params) SlotRoot(slot string) string { return filepath.Join(p.Dir, "slot-"+slot) }

func (p Params) FrontControl() string { return filepath.Join(p.Run, "front.sock") }

func (p Params) FrontState() string { return filepath.Join(p.Dir, "front.json") }

func (p Params) EnvFile(unit string) string { return filepath.Join(p.Dir, unit+hostsvc.EnvFileSuffix) }

func (p Params) BackupsDir() string { return filepath.Join(p.Dir, kcpdatastore.BackupsDir) }

func (p Params) SlotsFile() string { return filepath.Join(p.Dir, "kcp-slots.json") }

func (p Params) SharedDir() string { return filepath.Join(p.Dir, "shared") }

func (p Params) RollLock() string { return filepath.Join(p.Dir, "roll.lock") }

func (p Params) storeConfig(unit string, in pgunit.Install) kcpdatastore.PostgresConfig {
	return kcpdatastore.PostgresConfig{Unit: unit, Slice: p.App.Names().Slice(), Install: in, Data: p.PgData(), SocketDir: p.PgSocketDir()}
}

func (p Params) Postgres() (pgunit.Install, error) { return pgunit.FromRoot(p.PostgresRoot) }

func (p Params) shape() kcpserver.Shape {
	if p.App.SingleSlot() && !p.App.DevCore {
		return kcpserver.ShapeSingle
	}
	return kcpserver.ShapeBlueGreen
}

func (p Params) log(role string) hostsvc.Output {
	if p.Logs == "" {
		return hostsvc.Output{}
	}
	f := filepath.Join(p.Logs, role+".log")
	return hostsvc.Output{StdoutFile: f, StderrFile: f}
}

func (p Params) Spec(e table.Entry, frontURL string) (hostsvc.UnitSpec, error) {
	n := p.App.Names()
	var spec hostsvc.UnitSpec
	var err error
	switch e.Kind {
	case table.KindStore:
		var in pgunit.Install
		if in, err = pgunit.At(p.PostgresRoot); err == nil {
			spec, err = kcpdatastore.PostgresSpec(p.storeConfig(e.Unit, in))
		}
	case table.KindKine:
		kc := kcpdatastore.KineConfig{Unit: e.Unit, Slice: n.Slice(), Exec: p.KinePath, Dir: p.KineDir(e.Slot),
			Socket: p.KineSocket(e.Slot), PostgresSocketDir: p.PgSocketDir()}
		if p.App.Store == table.StorePostgres {
			spec, err = kcpdatastore.KinePostgresSpec(kc, n.Store())
		} else {
			spec, err = kcpdatastore.KineSqliteSpec(kc)
		}
	case table.KindFront:
		spec, err = kcpserver.FrontSpec(kcpserver.FrontConfig{Unit: e.Unit, Slice: n.Slice(), Exe: p.Exe, Owner: p.Owner,
			Address: p.FrontAddress, Control: p.FrontControl(), State: p.FrontState(), FDStore: p.FDStore})
	case table.KindSlot:
		spec, err = kcpserver.SlotSpec(kcpserver.SlotConfig{Unit: e.Unit, Slice: n.Slice(), Exec: p.KcpPath,
			Root: p.SlotRoot(e.Slot), EtcdServers: p.KineURL(p.App.KineOf(e.Slot)), FrontURL: frontURL, Shape: p.shape(),
			Wants: e.Wants})
		spec.After = e.After
	case table.KindCore:
		if !p.App.DevCore {
			return hostsvc.UnitSpec{}, errors.New("bootstrap: a production core is an installed unit of install's table, not a spec")
		}
		timeout := p.ReadyTimeout
		timeout = opt.Positive(timeout, DefaultReadyTimeout)
		spec = hostsvc.UnitSpec{Name: e.Unit, Description: "dev core of " + p.Owner,
			Exec:    []string{p.Exe, "internal", "dev-core", "--instance-dir", p.InstanceDir, "--ready-timeout", timeout.String()},
			WorkDir: p.InstanceDir, Slice: n.CoreSlice(), Restart: hostsvc.RestartOnFailure, KillSignal: syscall.SIGTERM,
			StopTimeout: CoreStopTimeout, After: e.After, Wants: e.Wants, Delegate: true, OOMPolicy: "continue"}
		if password := os.Getenv("ATPROTO_PASSWORD"); password != "" {
			spec.Env = append(spec.Env, "ATPROTO_PASSWORD="+password)
		}
	default:
		return hostsvc.UnitSpec{}, fmt.Errorf("bootstrap: unknown kind %q", e.Kind)
	}
	if err != nil {
		return hostsvc.UnitSpec{}, err
	}
	spec.Output = p.log(e.Role)
	return spec, nil
}

func (c Config) CoreSpec(rec *devinstance.Record) (hostsvc.UnitSpec, error) {
	if len(rec.StandIns) > 0 {
		var units []string
		for _, s := range rec.StandIns {
			units = append(units, c.Layout.Names().StandIn(s.Role))
		}
		p := c.Params(table.Dev(c.Layout.Short, table.StoreSqlite))
		return p.Spec(table.Entry{Kind: table.KindCore, Role: table.RoleCore, Unit: c.Layout.CoreUnit(), After: units, Wants: units}, "")
	}
	if rec.SharedKcp != "" {
		p := c.Params(table.Dev(c.Layout.Short, table.StoreSqlite))
		return p.Spec(table.Entry{Kind: table.KindCore, Role: table.RoleCore, Unit: c.Layout.CoreUnit()}, "")
	}
	app, err := c.App(rec)
	if err != nil {
		return hostsvc.UnitSpec{}, err
	}
	tbl, err := table.Table(app)
	if err != nil {
		return hostsvc.UnitSpec{}, err
	}
	return c.Params(app).Spec(tbl[len(tbl)-1], "")
}

func ProcSpecs(p Params, frontURL string) ([]hostsvc.UnitSpec, error) {
	tbl, err := table.Table(p.App)
	if err != nil {
		return nil, err
	}
	var out []hostsvc.UnitSpec
	for _, e := range tbl {
		if e.Kind == table.KindCore && !p.App.DevCore {
			continue
		}
		s, err := p.Spec(e, frontURL)
		if err != nil {
			return nil, err
		}
		out = append(out, s)
	}
	return out, nil
}

func Installed(p Params, frontURL string) ([]InstalledUnit, error) {
	tbl, err := table.Table(p.App)
	if err != nil {
		return nil, err
	}
	var out []InstalledUnit
	for _, e := range tbl {
		if e.Kind == table.KindCore {
			out = append(out, InstalledUnit{Entry: e, Unit: e.Unit, Path: filepath.Join(e.Unit+".d", DropInName),
				File: hostsvc.InstalledFile{Unit: hostsvc.RenderDropIn(e.After, nil)}})
			continue
		}
		pending := e.Kind == table.KindSlot && frontURL == ""
		url := frontURL
		if pending {
			url = PendingFrontURL
		}
		spec, err := p.Spec(e, url)
		if err != nil {
			return nil, err
		}
		f, err := hostsvc.RenderInstalled(spec, hostsvc.InstallOptions{EnvFile: p.EnvFile(e.Unit)})
		if err != nil {
			return nil, err
		}
		if pending {
			f.Env = nil
		}
		out = append(out, InstalledUnit{Entry: e, Unit: e.Unit, Path: e.Unit, Exec: spec.Exec[0], File: f})
	}
	return out, nil
}