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

import (
	"context"
	"errors"
	"fmt"
	"net"
	"path/filepath"
	"slices"
	"strconv"
	"time"

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

const (
	DefaultVaultReloadSettle = 10 * time.Second
	vaultReloadPoll          = 100 * time.Millisecond
)

var ErrVaultReloadDidNotTake = errors.New("vaultcert: the running vault was asked to reload its " +
	"TLS certificate and still does not present every host asked for")

var ErrNoVaultToReload = errors.New("vaultcert: no process holding the vault's port is this " +
	"cluster's vault")

type ServedVaultTLS struct {
	DataDir string

	Port int

	Hosts []string

	Reload func(ctx context.Context, dataDir string, port int) error

	Settle time.Duration
}

func EnsureServedVaultTLS(ctx context.Context, dataDir string, port int, hosts ...string) (VaultTLS, error) {
	return ServedVaultTLS{DataDir: dataDir, Port: port, Hosts: hosts}.Ensure(ctx)
}

func (s ServedVaultTLS) Ensure(ctx context.Context) (VaultTLS, error) {
	if s.Port <= 0 || s.Port > 65535 {
		return VaultTLS{}, fmt.Errorf("vaultcert: the vault's port %d is not a port, so there is no "+
			"running listener to check the hosts %v against", s.Port, s.Hosts)
	}
	tls, err := EnsureVaultTLSCertificate(s.DataDir, s.Hosts...)
	if err != nil {
		return VaultTLS{}, err
	}
	addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(s.Port))
	first := RequireServedVaultTLS(ctx, addr, s.Hosts...)
	if first == nil {
		return tls, nil
	}
	if !errors.Is(first, ErrVaultServesAnotherLeaf) {
		return VaultTLS{}, first
	}
	reload := s.Reload
	if reload == nil {
		reload = ReloadVault
	}
	if err := reload(ctx, s.DataDir, s.Port); err != nil {
		return VaultTLS{}, fmt.Errorf("%w. Reloading it was refused: %w", first, err)
	}
	settle := s.Settle
	settle = opt.Positive(settle, DefaultVaultReloadSettle)
	deadline := time.Now().Add(settle)
	last := first
	for time.Now().Before(deadline) {
		if err := ctx.Err(); err != nil {
			return VaultTLS{}, err
		}
		last = RequireServedVaultTLS(ctx, addr, s.Hosts...)
		if last == nil {
			return tls, nil
		}
		time.Sleep(vaultReloadPoll)
	}
	return VaultTLS{}, fmt.Errorf("%w within %s: %w. A reload that did not happen and one that "+
		"did look identical from the call that sent it, so the served leaf is what is read, and it "+
		"still lacks the host: restart the vault, or the cluster that holds it, and run this pass "+
		"again", ErrVaultReloadDidNotTake, settle, last)
}

func ReloadVault(_ context.Context, dataDir string, port int) error {
	want, err := clusterstack.Spec{OpenBaoConfig: clusterstack.OpenBaoConfigPath(dataDir)}.OpenBaoArgv()
	if err != nil {
		return err
	}
	pids, err := portscan.ListenerPIDs(port)
	if err != nil {
		return fmt.Errorf("vaultcert: finding the process listening on the vault's port %d: %w", port, err)
	}
	var vault []int
	var strangers []string
	for _, pid := range pids {
		argv, err := hostos.Cmdline(pid)
		if err != nil {
			strangers = append(strangers, fmt.Sprintf("pid %d (%v)", pid, err))
			continue
		}
		if isVaultArgv(argv, want) {
			if !slices.Contains(vault, pid) {
				vault = append(vault, pid)
			}
			continue
		}
		strangers = append(strangers, fmt.Sprintf("pid %d %v", pid, argv))
	}
	if len(vault) != 1 {
		return fmt.Errorf("%w: port %d is held by %d processes whose command line is %v and by %v. "+
			"A process that is not this cluster's vault is never signalled, and two that are cannot "+
			"both be this cluster's one listener", ErrNoVaultToReload, port, len(vault), want, strangers)
	}
	if err := hostos.Reload(vault[0]); err != nil {
		return fmt.Errorf("vaultcert: asking the vault at pid %d to reload its certificate: %w", vault[0], err)
	}
	return nil
}

func isVaultArgv(argv, want []string) bool {
	if len(argv) != len(want) || len(argv) == 0 {
		return false
	}
	if filepath.Base(argv[0]) != filepath.Base(want[0]) {
		return false
	}
	return slices.Equal(argv[1:], want[1:])
}