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

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

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

func Hostname(ctx context.Context, cfg Config, log *slog.Logger) error {
	if log == nil {
		log = slog.Default()
	}
	nodeName := os.Getenv("NODE_NAME")
	if nodeName == "" {
		env, err := readEnvFile(cfg.APIServerEnvPath)
		if err != nil {
			return fmt.Errorf("nodeagent: reading %s for NODE_NAME: %w", cfg.APIServerEnvPath, err)
		}
		nodeName = env["NODE_NAME"]
	}
	if !validName(nodeName) {
		return fmt.Errorf("nodeagent: NODE_NAME %q is not a node name", nodeName)
	}
	if len(nodeName) > 64 {
		return fmt.Errorf("nodeagent: node name %s is longer than the 64 bytes a hostname holds", nodeName)
	}
	current, err := os.Hostname()
	if err == nil && current == nodeName {
		return nil
	}
	if err := writeAtomic(cfg.HostnamePath, []byte(nodeName+"\n"), 0o644); err != nil {
		return err
	}
	if err := setHostname(nodeName); err != nil {
		return err
	}
	log.Info("hostname set to the node name", "node", nodeName)
	return nil
}

func DNSLink(ctx context.Context, cfg Config, log *slog.Logger) error {
	if log == nil {
		log = slog.Default()
	}
	_, _ = run(ctx, "modprobe", "dummy")
	if _, err := run(ctx, "ip", "link", "show", NodeLocalDNSLink); err != nil {
		if _, err := run(ctx, "ip", "link", "add", NodeLocalDNSLink, "type", "dummy"); err != nil {
			return err
		}
	}
	out, err := run(ctx, "ip", "-4", "addr", "show", "dev", NodeLocalDNSLink)
	if err != nil {
		return err
	}
	if !strings.Contains(string(out), " "+NodeLocalDNSAddress+"/32 ") {
		if _, err := run(ctx, "ip", "addr", "add", NodeLocalDNSAddress+"/32", "dev", NodeLocalDNSLink); err != nil {
			return err
		}
	}
	if _, err := run(ctx, "ip", "link", "set", NodeLocalDNSLink, "up"); err != nil {
		return err
	}
	log.Info("the per-node DNS address is on the dummy interface", "address", NodeLocalDNSAddress, "link", NodeLocalDNSLink)
	return nil
}

func Masquerade(ctx context.Context, cfg Config, log *slog.Logger) error {
	if log == nil {
		log = slog.Default()
	}
	if _, err := run(ctx, "sysctl", "-qw", "net.ipv4.ip_forward=1"); err != nil {
		return err
	}
	rule := []string{"-w", "-t", "nat", "-C", "POSTROUTING", "-s", PodClusterCIDR, "!", "-d", PodClusterCIDR,
		"-m", "comment", "--comment", PodEgressComment, "-j", "MASQUERADE"}
	if _, err := run(ctx, "iptables", rule...); err != nil {
		rule[3] = "-A"
		if _, err := run(ctx, "iptables", rule...); err != nil {
			return err
		}
	}
	log.Info("pod egress is masqueraded", "clusterCIDR", PodClusterCIDR, "comment", PodEgressComment)
	return nil
}

func InstallMeshd(ctx context.Context, cfg Config, log *slog.Logger) error {
	if log == nil {
		log = slog.Default()
	}
	if cfg.Meshd == nil {
		return fmt.Errorf("nodeagent: no socialweb-meshd pin is configured")
	}
	arch := ""
	switch runtime.GOARCH {
	case "amd64":
		arch = "x86_64"
	case "arm64":
		arch = "aarch64"
	default:
		return fmt.Errorf("nodeagent: no socialweb-meshd build for %s", runtime.GOARCH)
	}
	pin := cfg.Meshd.Sha256[arch]
	if executable(cfg.MeshdPath) {
		if got, err := sha256sum.File(cfg.MeshdPath); err == nil && got == pin {
			log.Info("socialweb-meshd is already installed", "sha256", pin)
			return nil
		}
	}
	base := strings.TrimRight(cfg.Meshd.DownloadBase, "/")
	url := base + "/downloads/" + pin + "/socialweb-meshd-" + arch
	dir := filepath.Dir(cfg.MeshdPath)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("nodeagent: creating %s: %w", dir, err)
	}
	var last error
	for attempt := 1; attempt <= 5; attempt++ {
		tmp, err := os.CreateTemp(dir, "."+filepath.Base(cfg.MeshdPath)+".download-*")
		if err != nil {
			return fmt.Errorf("nodeagent: creating a temporary file beside %s: %w", cfg.MeshdPath, err)
		}
		name := tmp.Name()
		got, err := fetchSHA256(ctx, url, tmp)
		if closeErr := tmp.Close(); err == nil {
			err = closeErr
		}
		if err == nil && got != pin {
			err = fmt.Errorf("nodeagent: the door served sha256 %s, not the pinned %s", got, pin)
		}
		if err == nil {
			if err := os.Chmod(name, 0o755); err != nil {
				os.Remove(name)
				return fmt.Errorf("nodeagent: setting the mode of %s: %w", name, err)
			}
			if err := os.Rename(name, cfg.MeshdPath); err != nil {
				os.Remove(name)
				return fmt.Errorf("nodeagent: renaming %s into place: %w", cfg.MeshdPath, err)
			}
			log.Info("installed socialweb-meshd", "sha256", pin, "arch", arch, "url", url)
			return nil
		}
		os.Remove(name)
		last = err
		if attempt < 5 {
			log.Info("the socialweb-meshd download failed; retrying", "url", url, "attempt", attempt,
				"of", 5, "err", err.Error())
			if err := clock.SleepErr(ctx, 2*time.Second); err != nil {
				return err
			}
		}
	}
	return fmt.Errorf("nodeagent: downloading %s: %w", url, last)
}

func fetchSHA256(ctx context.Context, url string, out io.Writer) (string, error) {
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return "", err
	}
	client := &http.Client{Timeout: 2 * time.Minute}
	response, err := client.Do(request)
	if err != nil {
		return "", err
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		return "", fmt.Errorf("HTTP %d from %s", response.StatusCode, url)
	}
	hash := sha256.New()
	if _, err := io.Copy(io.MultiWriter(out, hash), response.Body); err != nil {
		return "", err
	}
	return hex.EncodeToString(hash.Sum(nil)), nil
}