~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
166 lines · 4.1 KB Raw History
package devinstance

import (
	"cmp"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"

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

const Marker = ".swc-dev"

const (
	EnvFile     = "env"
	EnvJSONFile = "env.json"
)

func DevRoot(stateRoot string) string { return filepath.Join(stateRoot, "dev") }

func LinkMarker(worktree, state string) error {
	p := filepath.Join(worktree, Marker)
	if cur, err := os.Readlink(p); err == nil {
		if cur == state {
			return nil
		}
		if err := os.Remove(p); err != nil {
			return err
		}
	} else if _, err := os.Lstat(p); err == nil {
		return fmt.Errorf("devinstance: %s exists and is not a symlink; refusing to replace it", p)
	}
	return os.Symlink(state, p)
}

func UnlinkMarker(worktree, state string) bool {
	p := filepath.Join(worktree, Marker)
	if cur, err := os.Readlink(p); err != nil || cur != state {
		return false
	}
	return os.Remove(p) == nil
}

type Export struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

func envWord(s string) string {
	var b strings.Builder
	for _, r := range strings.ToUpper(s) {
		if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
			b.WriteRune(r)
			continue
		}
		b.WriteByte('_')
	}
	return b.String()
}

func Exports(rec Record) []Export {
	l := rec.Layout
	socket := l.Socket
	if rec.Parent != nil {
		socket = rec.Parent.Socket
	}
	out := []Export{
		{"SOCIALWEB_INSTANCE", l.ID},
		{"SOCIALWEB_DEV_STATE_DIR", l.StateDir},
		{"SOCIALWEB_DEVSUP_SOCKET", socket},
		{"KUBECONFIG", rec.AdminKubeconfig},
		{"SOCIALWEB_KCP_KUBECONFIG", rec.AdminKubeconfig},
		{"SOCIALWEB_KCP_HOST", rec.KcpURL},
		{"SOCIALWEB_KCP_ROOT", l.KcpRoot},
		{"SOCIALWEB_KCP_BASE_WORKSPACE", rec.BaseWorkspace},
		{"SOCIALWEB_KINE_URL", rec.KineURL},
		{"SOCIALWEB_CLUSTERPROVIDER_UNIT_PREFIX", l.Names().HostProcessPrefix()},
		{"SOCIALWEB_CLUSTERPROVIDER_HOSTPROCESS_WORKSPACE", rec.BaseWorkspace},
	}
	for _, u := range rec.Units {
		if u.Port > 0 {
			out = append(out, Export{"SOCIALWEB_DEV_PORT_" + envWord(u.Role), strconv.Itoa(u.Port)})
		}
	}
	if rec.VM != nil {
		ws := rec.BaseWorkspace
		ws = cmp.Or(ws, "root")
		host, _, _ := strings.Cut(strings.TrimSuffix(rec.KcpURL, "/"), "/clusters/")
		out = append(out,
			Export{"COMPUTE_PROVIDER_KCP", "true"},
			Export{"KCP_SERVER", host},
			Export{"KCP_WORKSPACE", ws},
			Export{"COMPUTE_PROVIDER_KCP_HANDLE", rec.VM.Handle})
	}
	return out
}

func ShellExports(ex []Export) string {
	var b strings.Builder
	for _, e := range ex {
		b.WriteString("export " + e.Name + "='" + strings.ReplaceAll(e.Value, "'", `'\''`) + "'\n")
	}
	return b.String()
}

func List(stateRoot string) ([]Record, error) {
	entries, err := os.ReadDir(DevRoot(stateRoot))
	if errors.Is(err, os.ErrNotExist) {
		return []Record{}, nil
	}
	if err != nil {
		return nil, err
	}
	out := []Record{}
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		var rec Record
		if err := fsx.ReadJSON(filepath.Join(DevRoot(stateRoot), e.Name(), "instance.json"), &rec); err != nil {
			continue
		}
		out = append(out, rec)
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Layout.ID < out[j].Layout.ID })
	return out, nil
}

type logName struct {
	role string
	unit string
	file string
}

func logNames(l Layout) []logName {
	return []logName{
		{"pg", l.Names().Store(), filepath.Join(l.Logs, "pg.log")},
		{"kine-a", l.KineUnit(), filepath.Join(l.Logs, "kine-a.log")},
		{"front", l.FrontUnit(), filepath.Join(l.Logs, "front.log")},
		{"kcp-a", l.KcpUnit(), filepath.Join(l.Logs, "kcp-a.log")},
		{"kcp-b", l.SlotUnit("b"), filepath.Join(l.Logs, "kcp-b.log")},
		{"core", l.CoreUnit(), filepath.Join(l.Logs, "core.log")},
		{"devsup", "", filepath.Join(l.SupState, "devsup.log")},
	}
}

func LogNames(l Layout) []string {
	var out []string
	for _, n := range logNames(l) {
		out = append(out, n.role)
	}
	return out
}

func LogFile(l Layout, name string) (string, error) {
	for _, n := range logNames(l) {
		if name == n.role || (n.unit != "" && name == n.unit) {
			return n.file, nil
		}
	}
	return "", fmt.Errorf("devinstance: no unit %q in instance %s: want one of %s or its unit name", name, l.ID, strings.Join(LogNames(l), ", "))
}