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

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

	"github.com/johnandersen777/socialweb-computer-kcp/internal/identitybundle"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/instance"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/materialiser"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/procgroup"
)

var (
	ErrNoRowDeleter = errors.New(
		"clusterprovider: the datastore holds a bootstrap row encrypted with the previous " +
			"instance's token and no row deleter is wired, so k3s would exit immediately; " +
			"see internal/instance")
	ErrNoBundleSeeder = errors.New(
		"clusterprovider: the identity bundle must be pre-seeded before k3s starts and no " +
			"seeder is wired; see internal/identitybundle")
	ErrNoBundle = errors.New(
		"clusterprovider: no identity bundle source was supplied, so the pre-seeded entries " +
			"cannot be verified; an unverified bundle is what GuardFor exists to refuse")
	ErrNoTokenSource = errors.New(
		"clusterprovider: no token source was supplied, so instance.PlanFor cannot answer; the " +
			"token plan is not optional because a k3s started with the wrong token dies on the " +
			"next start with 'bootstrap data already found and encrypted with different token'")
)

type GuardDeps struct {
	Bundle func(cluster string) (*identitybundle.Bundle, error)

	StateDir string

	Token func(cluster string) instance.Token

	HasRow func(ctx context.Context, cluster string) bool

	DeleteRow func(ctx context.Context, cluster string) error

	SeedBundle func(ctx context.Context, cluster string) error

	Log *slog.Logger
}

func GuardFor(d GuardDeps) materialiser.IdentityGuard {
	return func(ctx context.Context, o materialiser.Observed) error {
		cluster := o.Cluster.Name
		log := d.Log
		if log == nil {
			log = slog.Default()
		}
		dir, err := ClusterDataDir(d.StateDir, cluster)
		if err != nil {
			return err
		}
		if d.Token == nil {
			return fmt.Errorf("%w (cluster %s)", ErrNoTokenSource, cluster)
		}
		if d.Bundle == nil {
			return fmt.Errorf("%w (cluster %s)", ErrNoBundle, cluster)
		}

		if uid := string(o.Cluster.UID); uid != "" {
			bdir, err := BundleDir(d.StateDir, cluster)
			if err != nil {
				return err
			}
			adopted, err := identitybundle.BindClusterUID(bdir, uid)
			if err != nil {
				return fmt.Errorf("clusterprovider: %s: refusing to seed or verify it: %w", cluster, err)
			}
			if adopted {
				log.Info("guard: identity bundle adopted for uid", "cluster", cluster, "uid", uid)
			}
		}

		plan, err := instance.PlanFor(d.Token(cluster), d.HasRow != nil && d.HasRow(ctx, cluster))
		if err != nil {
			return fmt.Errorf("clusterprovider: the token plan for %s: %w", cluster, err)
		}
		log.Info("guard: token plan", "cluster", cluster, "dataDir", dir,
			"deleteBootstrapRow", plan.DeleteBootstrapRow, "reSeedBundle", plan.ReSeedBundle,
			"reason", plan.Reason)

		if plan.DeleteBootstrapRow {
			if d.DeleteRow == nil {
				return fmt.Errorf("%w (cluster %s)", ErrNoRowDeleter, cluster)
			}
			if err := d.DeleteRow(ctx, cluster); err != nil {
				return fmt.Errorf("clusterprovider: deleting the bootstrap row for %s: %w", cluster, err)
			}
		}
		if plan.ReSeedBundle {
			if d.SeedBundle == nil {
				return fmt.Errorf("%w (cluster %s)", ErrNoBundleSeeder, cluster)
			}
			if err := d.SeedBundle(ctx, cluster); err != nil && !errors.Is(err, ErrNoBundleYet) {
				return fmt.Errorf("clusterprovider: seeding the identity bundle for %s: %w", cluster, err)
			}
		}

		b, err := d.Bundle(cluster)
		if errors.Is(err, ErrNoBundleYet) {
			log.Warn("guard: no identity bundle is persisted for this cluster yet, so there is "+
				"nothing to verify. k3s authors the 13 entries on a datastore with no bootstrap row, "+
				"and the harvest after the group is up is what persists them; every later start "+
				"seeds from that copy and verifies it byte for byte",
				"cluster", cluster, "bundleDir", dir)
			return nil
		}
		if err != nil {
			return err
		}
		root, err := bundleRoot(d.StateDir, cluster)
		if err != nil {
			return err
		}
		return b.Verify(root)
	}
}

var ErrNoHarvestSeam = errors.New(
	"clusterprovider: this Stack puts no k3s step in the group, so there is no moment after " +
		"the CAs exist and before anything else could need them, and the harvest would SILENTLY " +
		"NOT RUN -- a bundle harvested by nothing and a bundle harvested correctly look " +
		"identical from a green suite")

func HarvestAfterK3s(steps []procgroup.Step, harvest func(ctx context.Context, cluster string) error, cluster string) ([]procgroup.Step, error) {
	if harvest == nil {
		return nil, fmt.Errorf(
			"clusterprovider: this Stack has a k3s step and no identity-bundle harvest, so the " +
				"cluster's CAs would never be persisted and every rebuild would be a NEW cluster " +
				"under the same name -- invalidating every kubeconfig issued before it")
	}
	idx := -1
	for i, s := range steps {
		if s.Name == procgroup.ComponentK3s {
			idx = i
			break
		}
	}
	if idx < 0 {
		return nil, ErrNoHarvestSeam
	}
	inner := steps[idx].Ready
	steps[idx].Ready = func(ctx context.Context, p procgroup.Member) error {
		if inner != nil {
			if err := inner(ctx, p); err != nil {
				return err
			}
		}
		return harvest(ctx, cluster)
	}
	return steps, nil
}