~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
143 lines · 4.7 KB Raw History
package clusterprovider

import (
	"context"
	"errors"
	"fmt"
	"log/slog"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/nodemarket"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/vaultlayout"
)

type NodeVault struct {
	units   *UnitsHost
	cluster string
	dataDir string
	log     *slog.Logger
}

var _ nodemarket.NodeSecrets = (*NodeVault)(nil)

var _ nodemarket.NodeSecretsReconciler = (*NodeVault)(nil)

func NewNodeVault(units *UnitsHost, cluster, dataDir string) *NodeVault {
	return &NodeVault{units: units, cluster: cluster, dataDir: dataDir, log: slog.Default()}
}

func (v *NodeVault) WithLogger(log *slog.Logger) *NodeVault {
	if log != nil {
		v.log = log
	}
	return v
}

func (v *NodeVault) declare(ctx context.Context, change func(map[string]*v1alpha1.NodeClaimGrant)) error {
	if v.units == nil {
		return fmt.Errorf("%w (cluster %s): no units host", ErrNoClusterVault, v.cluster)
	}
	return v.units.UpdateVaultDeclared(ctx, v.cluster, func(d *v1alpha1.OpenBaoServerDeclared) {
		nodes := vaultNodes(d)
		change(nodes)
		setVaultNodes(d, nodes)
	})
}

func (v *NodeVault) EnsurePolicy(ctx context.Context, node string) error {
	if err := v.declare(ctx, func(nodes map[string]*v1alpha1.NodeClaimGrant) {
		if _, ok := nodes[node]; !ok {
			nodes[node] = nil
		}
	}); err != nil {
		return fmt.Errorf("clusterprovider: declaring policy %s in the cluster vault of %s: %w", vaultlayout.NodeJWTPolicyName(node), v.cluster, err)
	}
	return nil
}

func (v *NodeVault) WriteSecret(ctx context.Context, node string, secret nodemarket.NodeSecret) error {
	client, _, err := clusterVault(ctx, v.units.c.Logins, v.dataDir)
	if err != nil {
		return err
	}
	data := map[string]any{
		"token":            secret.Token,
		"nodePassword":     secret.NodePassword,
		"serverCA":         secret.ServerCA,
		"apiserverTicket":  secret.ApiserverTicket,
		"advertiseAddress": secret.AdvertiseAddress,
		"apiserverPort":    secret.ApiserverPort,
	}
	if err := client.PutKV(ctx, vaultlayout.NodeKVDataPath(node), data); err != nil {
		return fmt.Errorf("clusterprovider: writing %s: %w", vaultlayout.NodeKVDataPath(node), err)
	}
	return nil
}

func (v *NodeVault) EnsureLogin(ctx context.Context, node string, grant v1alpha1.NodeClaimGrant) (nodemarket.NodeLogin, error) {
	if !grantUsable(&grant) {
		return nodemarket.NodeLogin{}, fmt.Errorf("clusterprovider: a JWT role for node %s needs an "+
			"issuer, a subject and an audience, and the grant carries %q, %q, %q", node,
			grant.Issuer, grant.Subject, grant.Audience)
	}
	g := grant
	if err := v.declare(ctx, func(nodes map[string]*v1alpha1.NodeClaimGrant) { nodes[node] = &g }); err != nil {
		return nodemarket.NodeLogin{}, fmt.Errorf("clusterprovider: declaring the JWT login of node %s in the cluster vault of %s "+
			"(OpenBao fetches the discovery document of %s when the mount is configured, so the bidder's issuer must answer "+
			"from this host): %w", node, v.cluster, grant.Issuer, err)
	}
	return nodemarket.NodeLogin{Mount: vaultlayout.NodeJWTMount(grant.Issuer), Role: vaultlayout.NodeJWTRoleName(node),
		Policy: vaultlayout.NodeJWTPolicyName(node)}, nil
}

func (v *NodeVault) Revoke(ctx context.Context, node string) error {
	if err := v.declare(ctx, func(nodes map[string]*v1alpha1.NodeClaimGrant) { delete(nodes, node) }); err != nil {
		return fmt.Errorf("clusterprovider: removing node %s from the cluster vault of %s: %w", node, v.cluster, err)
	}
	v.log.Info("clusterprovider: node removed from the cluster vault's declaration; the sync deleted its login, policy and secret",
		"cluster", v.cluster, "node", node)
	return nil
}

func (v *NodeVault) DeclaredNodes(ctx context.Context) (map[string]bool, error) {
	if v.units == nil {
		return nil, fmt.Errorf("%w (cluster %s): no units host", ErrNoClusterVault, v.cluster)
	}
	cur, err := v.units.vaultServer(ctx, v.cluster)
	if err != nil {
		return nil, err
	}
	if cur == nil {
		return nil, fmt.Errorf("%w (cluster %s)", ErrNoClusterVault, v.cluster)
	}
	out := map[string]bool{}
	for n, g := range vaultNodes(cur.Spec.Declared) {
		out[n] = grantUsable(g)
	}
	return out, nil
}

func (v *NodeVault) Reconcile(ctx context.Context, live map[string]*v1alpha1.NodeClaimGrant) error {
	err := v.declare(ctx, func(nodes map[string]*v1alpha1.NodeClaimGrant) {
		prev := map[string]*v1alpha1.NodeClaimGrant{}
		for n, g := range nodes {
			prev[n] = g
			delete(nodes, n)
		}
		for n, g := range live {
			switch {
			case grantUsable(g):
				c := *g
				nodes[n] = &c
			case grantUsable(prev[n]):
				nodes[n] = prev[n]
			default:
				nodes[n] = nil
			}
		}
	})
	if errors.Is(err, ErrNoClusterVault) && len(live) == 0 {
		return nil
	}
	return err
}