~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
220 lines · 6.7 KB Raw History
package install

import (
	"cmp"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"sort"
	"strings"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/bootstrap/table"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/buildstamp"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/hostcontract"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/hostsvc"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/sha256sum"
)

var (
	ErrManifestMismatch = errors.New("upgrade: the binary does not match the sha256 it was published with")
	ErrSelfCheck        = errors.New("upgrade: the new binary's self-check failed")
	ErrNoPrevious       = errors.New("rollback: bin/previous names no binary")
	ErrNoCoreUnit       = errors.New("no enabled core unit")
	ErrDevBinary        = errors.New("the binary is a dev build (version says \"dev build, not reproducible\"); install and upgrade take a release build unless --allow-dev-binary is given (dev and test runs only)")
	digestName          = regexp.MustCompile(`^[0-9a-f]{64}$`)
)

type UpgradeOptions struct {
	StateDir   string
	Home       string
	Account    string
	Root       string
	RuntimeDir string
	Proc       string
	Binary     string
	SHA256     string
	AllowDev   bool
	Out        io.Writer
	DevStamp   func(ctx context.Context, exe string) (*buildstamp.Info, error)
	SelfCheck  func(ctx context.Context, exe string, role hostcontract.Role, stateDir string) error
	Restart    func(ctx context.Context, units []string) error
}

func (o UpgradeOptions) out() io.Writer {
	if o.Out == nil {
		return io.Discard
	}
	return o.Out
}

func (o UpgradeOptions) role() hostcontract.Role {
	if o.Account != "" {
		if rec, err := ReadRecord(cmp.Or(o.Root, "/"), o.Account); err == nil && rec.Role != "" && rec.StateDir == o.StateDir {
			return rec.Role
		}
	}
	return InstalledRole(o.Home)
}

func (o UpgradeOptions) bin() string { return filepath.Join(o.StateDir, "bin") }

func isGatewayCore(name string) bool {
	return strings.HasPrefix(name, RuntimePrefix) && strings.HasSuffix(name, "-"+table.GatewayCore)
}

func isBidderCore(name string) bool {
	return strings.HasPrefix(name, RuntimePrefix) && strings.Contains(name, "-"+table.BidderCore) && strings.HasSuffix(name, hostsvc.ServiceSuffix)
}

func isRequesterCore(name string) bool {
	return strings.HasPrefix(name, RuntimePrefix) && strings.Contains(name, "-"+table.RequesterCore) && strings.HasSuffix(name, hostsvc.ServiceSuffix)
}

func InstalledRole(home string) hostcontract.Role {
	entries, _ := os.ReadDir(filepath.Join(home, UserUnitDir))
	for _, e := range entries {
		if isGatewayCore(e.Name()) {
			return hostcontract.RoleGateway
		}
	}
	for _, e := range entries {
		if isRequesterCore(e.Name()) {
			return hostcontract.RoleRequester
		}
	}
	return hostcontract.RoleBidder
}

func EnabledCoreUnits(home string) []string {
	entries, _ := os.ReadDir(filepath.Join(home, UserUnitDir, WantsDir))
	var out []string
	for _, e := range entries {
		if isGatewayCore(e.Name()) || isBidderCore(e.Name()) || isRequesterCore(e.Name()) {
			out = append(out, e.Name())
		}
	}
	sort.Strings(out)
	return out
}

func readTarget(link string) string {
	t, err := os.Readlink(link)
	if err != nil || !digestName.MatchString(t) {
		return ""
	}
	return t
}

func Upgrade(ctx context.Context, o UpgradeOptions) error {
	if !digestName.MatchString(o.SHA256) {
		return fmt.Errorf("upgrade: --sha256 must name the published 64-hex digest of the binary, got %q", o.SHA256)
	}
	sum, err := sha256sum.File(o.Binary)
	if err != nil {
		return err
	}
	if sum != o.SHA256 {
		return fmt.Errorf("%w: %s is %s, published %s", ErrManifestMismatch, o.Binary, sum, o.SHA256)
	}
	if !o.AllowDev && o.DevStamp != nil {
		stamp, err := o.DevStamp(ctx, o.Binary)
		if err != nil {
			return fmt.Errorf("%w: %s version --json: %v", ErrSelfCheck, o.Binary, err)
		}
		if stamp != nil {
			return fmt.Errorf("upgrade: %w: %s (commit %s, built %s)", ErrDevBinary, o.Binary, stamp.Commit, stamp.BuiltUTC)
		}
	}
	current := readTarget(filepath.Join(o.bin(), "current"))
	if current == sum {
		fmt.Fprintf(o.out(), "upgrade: bin/current is already %s; nothing to switch, and a running core is left "+
			"running. If its units are not up, restart them: systemctl --user restart <unit>\n", sum)
		return nil
	}
	dir := filepath.Join(o.bin(), sum)
	dst := filepath.Join(dir, CoreName)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}
	if have, _ := sha256sum.File(dst); have != sum {
		if err := copyFile(o.Binary, dst, 0o755); err != nil {
			return err
		}
	}
	if have, err := sha256sum.File(dst); err != nil || have != sum {
		return fmt.Errorf("%w: the copy at %s is %s", ErrManifestMismatch, dst, have)
	}
	role := o.role()
	if err := o.SelfCheck(ctx, dst, role, o.StateDir); err != nil {
		return fmt.Errorf("%w: %s: %v; bin/current still names %s", ErrSelfCheck, dst, err, current)
	}
	if current != "" {
		if err := retarget(filepath.Join(o.bin(), "previous"), current); err != nil {
			return err
		}
	}
	if err := retarget(filepath.Join(o.bin(), "current"), sum); err != nil {
		return err
	}
	fmt.Fprintf(o.out(), "upgrade: bin/current -> %s (previous %s)\n", sum, current)
	if err := o.restart(ctx); err != nil {
		return err
	}
	o.collect()
	return nil
}

func (o UpgradeOptions) restart(ctx context.Context) error {
	units := EnabledCoreUnits(o.Home)
	if len(units) == 0 {
		return fmt.Errorf("%w under %s: bin/current is switched but nothing was restarted; restart the core unit by hand",
			ErrNoCoreUnit, filepath.Join(o.Home, UserUnitDir, WantsDir))
	}
	fmt.Fprintf(o.out(), "restarting %s\n", strings.Join(units, " "))
	return o.Restart(ctx, units)
}

func Rollback(ctx context.Context, o UpgradeOptions) error {
	previous := readTarget(filepath.Join(o.bin(), "previous"))
	if previous == "" {
		return ErrNoPrevious
	}
	if _, err := os.Stat(filepath.Join(o.bin(), previous, CoreName)); err != nil {
		return fmt.Errorf("%w: %v", ErrNoPrevious, err)
	}
	current := readTarget(filepath.Join(o.bin(), "current"))
	if err := retarget(filepath.Join(o.bin(), "current"), previous); err != nil {
		return err
	}
	if current != "" {
		if err := retarget(filepath.Join(o.bin(), "previous"), current); err != nil {
			return err
		}
	}
	fmt.Fprintf(o.out(), "rollback: bin/current -> %s (previous %s)\n", previous, current)
	if err := o.restart(ctx); err != nil {
		return err
	}
	o.collect()
	return nil
}

func DevStamp(ctx context.Context, exe string) (*buildstamp.Info, error) {
	b, err := exec.CommandContext(ctx, exe, "version", "--json").Output()
	if err != nil {
		return nil, err
	}
	var v struct {
		Dev *buildstamp.Info `json:"dev"`
	}
	if err := json.Unmarshal(b, &v); err != nil {
		return nil, err
	}
	return v.Dev, nil
}