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

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"

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

type RollMode string

const (
	ModeBlueGreen     RollMode = "blue-green"
	ModePlannedOutage RollMode = "planned-outage"
)

var ErrIncompatiblePair = errors.New("kcpserver: no storage-compatible kcpCompat record for this pair of kcp releases")

type Asset struct {
	Path    string `json:"path"`
	SHA256  string `json:"sha256"`
	Version string `json:"version"`
}

var releaseRE = regexp.MustCompile(`kcp-(v[0-9]+\.[0-9]+\.[0-9]+[0-9A-Za-z.-]*)`)

func Release(version string) string {
	if m := releaseRE.FindStringSubmatch(version); m != nil {
		return m[1]
	}
	return ""
}

func ReadAsset(ctx context.Context, path string) (Asset, error) {
	if !filepath.IsAbs(path) {
		return Asset{}, fmt.Errorf("kcpserver: kcp executable %q must be an absolute path", path)
	}
	real, err := filepath.EvalSymlinks(path)
	if err != nil {
		return Asset{}, err
	}
	f, err := os.Open(real)
	if err != nil {
		return Asset{}, err
	}
	defer f.Close()
	fi, err := f.Stat()
	if err != nil || fi.IsDir() || fi.Mode()&0o111 == 0 {
		return Asset{}, fmt.Errorf("kcpserver: %s is not an executable file", real)
	}
	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return Asset{}, err
	}
	vctx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()
	out, _ := exec.CommandContext(vctx, real, "--version").CombinedOutput()
	return Asset{Path: real, SHA256: hex.EncodeToString(h.Sum(nil)), Version: strings.TrimSpace(string(out))}, nil
}

type PairVerdict struct {
	Mode   RollMode            `json:"mode"`
	From   string              `json:"from"`
	To     string              `json:"to"`
	Reason string              `json:"reason"`
	Record *versions.KcpCompat `json:"record,omitempty"`
}

func CheckPair(from, to Asset, records []versions.KcpCompat, plannedOutage bool) (PairVerdict, error) {
	v := PairVerdict{From: Release(from.Version), To: Release(to.Version)}
	compatible := false
	switch {
	case from.SHA256 != "" && from.SHA256 == to.SHA256:
		compatible, v.Reason = true, "same asset"
	case v.From != "" && v.From == v.To:
		compatible, v.Reason = true, "same kcp release "+v.From+", a rebuilt binary"
	default:
		for i := range records {
			r := records[i]
			if (r.From == v.From && r.To == v.To) || (r.From == v.To && r.To == v.From) {
				v.Record = &r
				compatible = r.StorageCompatible
				v.Reason = fmt.Sprintf("kcpCompat %s -> %s storageCompatible=%t (diff sha256 %s)", r.From, r.To, r.StorageCompatible, r.DiffSHA256)
				break
			}
		}
		if v.Record == nil {
			v.Reason = fmt.Sprintf("no kcpCompat record for %q -> %q", v.From, v.To)
		}
	}
	if plannedOutage {
		v.Mode = ModePlannedOutage
		return v, nil
	}
	if !compatible {
		return v, fmt.Errorf("%w: %s; the only path is --planned-outage (pg_dump, stop the old slot, start the new one on the same store)", ErrIncompatiblePair, v.Reason)
	}
	v.Mode = ModeBlueGreen
	return v, nil
}