~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
329 lines · 9.2 KB Raw History
package nodeagent

import (
	"cmp"
	"fmt"
	"net"
	"path/filepath"
	"regexp"
	"time"

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

const (
	DefaultNodeDir = "/etc/socialweb/node"

	DefaultK3sDataDir = "/var/lib/rancher/k3s"

	DefaultOpenBaoLinkAddress = "127.0.0.1:18200"

	DefaultGuestLoginHelper = "/usr/local/bin/guest-login.sh"

	DefaultK3sPath = "/usr/local/bin/k3s"

	DefaultDumbpipePath = "/usr/local/bin/dumbpipe"

	DefaultMeshdPath = "/usr/local/bin/socialweb-meshd"

	DefaultNodeBinaryPath = "/usr/local/bin/socialweb-node"

	DefaultCNIConfDir = "/etc/cni/net.d"

	DefaultCNIBinDir = "/opt/cni/bin"

	DefaultK3sNodePasswordPath = "/etc/rancher/node/password"

	DefaultDoneStamp = "/var/lib/socialweb-node-secrets.done"

	DefaultHostnamePath = "/etc/hostname"

	DefaultLinkWaitAttempts = 120

	DefaultLinkWaitInterval = 500 * time.Millisecond

	DefaultCNIWaitAttempts = 90

	DefaultCNIWaitInterval = 2 * time.Second

	DefaultDiagnoseAttempts = 6

	DefaultDiagnoseInterval = 20 * time.Second

	DefaultGuestLoginWaitAttempts = 60

	DefaultGuestLoginWaitInterval = 2 * time.Second

	DefaultCNIRetryInterval = 2 * time.Second

	DefaultUnitDir = "/etc/systemd/system"

	PodClusterCIDR = "10.42.0.0/16"

	PodMTU = 1160

	PodBridge = "cni0"

	PodEgressComment = "socialweb-pod-egress"

	CNIConflistName = "10-socialweb-bridge.conflist"

	NodeLocalDNSAddress = "169.254.20.10"

	NodeLocalDNSLink = "swdns0"
)

var (
	sha256Pattern = regexp.MustCompile(`^[0-9a-f]{64}$`)

	originPattern = regexp.MustCompile(`^https?://[A-Za-z0-9.-]+(:[0-9]{1,5})?$`)

	safeNamePattern = regexp.MustCompile(`^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$`)

	ipv4AddressPattern = regexp.MustCompile(`^([0-9]{1,3}\.){3}[0-9]{1,3}$`)

	portPattern = regexp.MustCompile(`^[0-9]{1,5}$`)

	ticketPattern = regexp.MustCompile(`^[a-z0-9]+$`)
)

type MeshdPin struct {
	DownloadBase string `json:"downloadBase"`

	Sha256 map[string]string `json:"sha256"`
}

type NodeAgentPin struct {
	DownloadBase string `json:"downloadBase"`

	Sha256 map[string]string `json:"sha256"`
}

func ValidateMeshdPin(pin *MeshdPin) error {
	if pin == nil {
		return nil
	}
	return ValidatePin("socialweb-meshd", pin.DownloadBase, pin.Sha256)
}

func ValidateNodeAgentPin(pin *NodeAgentPin) error {
	if pin == nil {
		return fmt.Errorf("nodeagent: the socialweb-node pin is missing")
	}
	return ValidatePin("socialweb-node", pin.DownloadBase, pin.Sha256)
}

func ValidatePin(kind, downloadBase string, sha map[string]string) error {
	if !originPattern.MatchString(downloadBase) {
		return fmt.Errorf("nodeagent: %s downloadBase %q is not an http(s) origin with no path", kind, downloadBase)
	}
	for _, arch := range []string{"x86_64", "aarch64"} {
		if !sha256Pattern.MatchString(sha[arch]) {
			return fmt.Errorf("nodeagent: %s sha256 for %s is not 64 lowercase hex characters", kind, arch)
		}
	}
	return nil
}

type Config struct {
	ClaimDocPath string `json:"claimDocPath"`

	ClaimCAPath string `json:"claimCAPath"`

	OpenBaoLinkAddress string `json:"openBaoLinkAddress"`

	OpenBaoTicketPath string `json:"openBaoTicketPath"`

	GuestLoginHelperPath string `json:"guestLoginHelperPath"`

	K3sDataDir string `json:"k3sDataDir"`

	NodeDir string `json:"nodeDir"`

	CNIConfDir string `json:"cniConfDir"`

	CNIBinDir string `json:"cniBinDir"`

	KubeletKubeconfig string `json:"kubeletKubeconfig"`

	APIServerEnvPath string `json:"apiserverEnvPath"`

	TokenPath string `json:"tokenPath"`

	NodePasswordPath string `json:"nodePasswordPath"`

	ServerCAPath string `json:"serverCAPath"`

	K3sNodePasswordPath string `json:"k3sNodePasswordPath"`

	MeshdEnvPath string `json:"meshdEnvPath"`

	DoneStamp string `json:"doneStamp"`

	K3sPath string `json:"k3sPath"`

	DumbpipePath string `json:"dumbpipePath"`

	MeshdPath string `json:"meshdPath"`

	HostnamePath string `json:"hostnamePath"`

	NodeBinaryPath string `json:"nodeBinaryPath"`

	ConfigPath string `json:"configPath"`

	LinkWaitAttempts int `json:"linkWaitAttempts"`

	LinkWaitIntervalMS int `json:"linkWaitIntervalMs"`

	CNIWaitAttempts int `json:"cniWaitAttempts"`

	CNIWaitIntervalMS int `json:"cniWaitIntervalMs"`

	DiagnoseAttempts int `json:"diagnoseAttempts"`

	DiagnoseIntervalSeconds int `json:"diagnoseIntervalSeconds"`

	GuestLoginWaitAttempts int `json:"guestLoginWaitAttempts"`

	GuestLoginWaitIntervalSeconds int `json:"guestLoginWaitIntervalSeconds"`

	Meshd *MeshdPin `json:"meshd,omitempty"`
}

func (c Config) WithDefaults() Config {
	c.NodeDir = cmp.Or(c.NodeDir, DefaultNodeDir)
	inDir := func(name string) string { return filepath.Join(c.NodeDir, name) }
	if c.ClaimDocPath == "" {
		c.ClaimDocPath = inDir("claim.json")
	}
	if c.ClaimCAPath == "" {
		c.ClaimCAPath = inDir("openbao-ca.pem")
	}
	if c.OpenBaoTicketPath == "" {
		c.OpenBaoTicketPath = inDir("openbao-ticket")
	}
	c.OpenBaoLinkAddress = cmp.Or(c.OpenBaoLinkAddress, DefaultOpenBaoLinkAddress)
	c.GuestLoginHelperPath = cmp.Or(c.GuestLoginHelperPath, DefaultGuestLoginHelper)
	c.K3sDataDir = cmp.Or(c.K3sDataDir, DefaultK3sDataDir)
	c.CNIConfDir = cmp.Or(c.CNIConfDir, DefaultCNIConfDir)
	c.CNIBinDir = cmp.Or(c.CNIBinDir, DefaultCNIBinDir)
	if c.KubeletKubeconfig == "" {
		c.KubeletKubeconfig = filepath.Join(c.K3sDataDir, "agent", "kubelet.kubeconfig")
	}
	if c.APIServerEnvPath == "" {
		c.APIServerEnvPath = inDir("apiserver.env")
	}
	if c.TokenPath == "" {
		c.TokenPath = inDir("token")
	}
	if c.NodePasswordPath == "" {
		c.NodePasswordPath = inDir("node-password")
	}
	if c.ServerCAPath == "" {
		c.ServerCAPath = inDir("server-ca.pem")
	}
	c.K3sNodePasswordPath = cmp.Or(c.K3sNodePasswordPath, DefaultK3sNodePasswordPath)
	if c.MeshdEnvPath == "" {
		c.MeshdEnvPath = inDir("meshd.env")
	}
	c.DoneStamp = cmp.Or(c.DoneStamp, DefaultDoneStamp)
	c.K3sPath = cmp.Or(c.K3sPath, DefaultK3sPath)
	c.DumbpipePath = cmp.Or(c.DumbpipePath, DefaultDumbpipePath)
	c.MeshdPath = cmp.Or(c.MeshdPath, DefaultMeshdPath)
	c.HostnamePath = cmp.Or(c.HostnamePath, DefaultHostnamePath)
	c.NodeBinaryPath = cmp.Or(c.NodeBinaryPath, DefaultNodeBinaryPath)
	if c.ConfigPath == "" {
		c.ConfigPath = inDir("join.json")
	}
	c.LinkWaitAttempts = opt.Positive(c.LinkWaitAttempts, DefaultLinkWaitAttempts)
	if c.LinkWaitIntervalMS <= 0 {
		c.LinkWaitIntervalMS = int(DefaultLinkWaitInterval / time.Millisecond)
	}
	c.CNIWaitAttempts = opt.Positive(c.CNIWaitAttempts, DefaultCNIWaitAttempts)
	if c.CNIWaitIntervalMS <= 0 {
		c.CNIWaitIntervalMS = int(DefaultCNIWaitInterval / time.Millisecond)
	}
	c.DiagnoseAttempts = opt.Positive(c.DiagnoseAttempts, DefaultDiagnoseAttempts)
	if c.DiagnoseIntervalSeconds <= 0 {
		c.DiagnoseIntervalSeconds = int(DefaultDiagnoseInterval / time.Second)
	}
	c.GuestLoginWaitAttempts = opt.Positive(c.GuestLoginWaitAttempts, DefaultGuestLoginWaitAttempts)
	if c.GuestLoginWaitIntervalSeconds <= 0 {
		c.GuestLoginWaitIntervalSeconds = int(DefaultGuestLoginWaitInterval / time.Second)
	}
	return c
}

func (c Config) Validate() error {
	absolute := map[string]string{
		"claimDocPath": c.ClaimDocPath, "claimCAPath": c.ClaimCAPath, "k3sDataDir": c.K3sDataDir,
		"nodeDir": c.NodeDir, "cniConfDir": c.CNIConfDir, "cniBinDir": c.CNIBinDir,
		"kubeletKubeconfig": c.KubeletKubeconfig, "apiserverEnvPath": c.APIServerEnvPath,
		"tokenPath": c.TokenPath, "nodePasswordPath": c.NodePasswordPath, "serverCAPath": c.ServerCAPath,
		"k3sNodePasswordPath": c.K3sNodePasswordPath, "meshdEnvPath": c.MeshdEnvPath,
		"doneStamp": c.DoneStamp, "k3sPath": c.K3sPath, "dumbpipePath": c.DumbpipePath,
		"meshdPath": c.MeshdPath, "hostnamePath": c.HostnamePath, "nodeBinaryPath": c.NodeBinaryPath,
		"openBaoTicketPath": c.OpenBaoTicketPath, "configPath": c.ConfigPath,
	}
	for name, value := range absolute {
		if value == "" || !filepath.IsAbs(value) {
			return fmt.Errorf("nodeagent: %s %q is not an absolute path", name, value)
		}
	}
	if host, port, err := net.SplitHostPort(c.OpenBaoLinkAddress); err != nil || !validIPv4(host) || !validPort(port) {
		return fmt.Errorf("nodeagent: openBaoLinkAddress %q is not an IPv4 address and port", c.OpenBaoLinkAddress)
	}
	if err := ValidateMeshdPin(c.Meshd); err != nil {
		return err
	}
	return nil
}

func (c Config) LinkWaitInterval() time.Duration {
	return time.Duration(c.LinkWaitIntervalMS) * time.Millisecond
}

func (c Config) CNIWaitInterval() time.Duration {
	return time.Duration(c.CNIWaitIntervalMS) * time.Millisecond
}

func (c Config) DiagnoseInterval() time.Duration {
	return time.Duration(c.DiagnoseIntervalSeconds) * time.Second
}

func (c Config) GuestLoginWaitInterval() time.Duration {
	return time.Duration(c.GuestLoginWaitIntervalSeconds) * time.Second
}

func (c Config) CNIPluginPath() string {
	return filepath.Join(c.K3sDataDir, "data", "current", "bin", "cni")
}

func validName(name string) bool { return safeNamePattern.MatchString(name) }

func validIPv4(value string) bool { return ipv4AddressPattern.MatchString(value) }

func validPort(value string) bool { return portPattern.MatchString(value) }

func validTicket(value string) bool { return ticketPattern.MatchString(value) }

func valueString(value any) (string, bool) {
	switch v := value.(type) {
	case string:
		if v == "" {
			return "", false
		}
		return v, true
	case float64:
		return fmt.Sprintf("%.0f", v), true
	case float32:
		return fmt.Sprintf("%.0f", v), true
	case int:
		return fmt.Sprintf("%d", v), true
	case int64:
		return fmt.Sprintf("%d", v), true
	default:
		return "", false
	}
}