~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
425 lines · 12.7 KB Raw History
package localrepo

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"maps"
	"slices"
	"strings"
	"sync"

	"github.com/bluesky-social/indigo/atproto/syntax"
	"k8s.io/apimachinery/pkg/runtime/schema"

	commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	atpds "github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/pds"
	atrepo "github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/repo"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/atprotorepo/scoped"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/bus"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/atprotorepo"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/pds"
	pdsctl "github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/pds"
	ctlruntime "github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/binding"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/conds"
)

const (
	Name = "localrepo"

	Export = "applications"

	Kind = "LocalRepo"

	PdsKind = "Pds"

	ReasonReady = "Ready"

	ReasonInvalidSpec = "InvalidSpec"

	ReasonLinkDenied = "LinkDenied"

	ReasonPdsUnavailable = "PdsUnavailable"

	ReasonAccountMissing = "AccountMissing"

	ReasonAccountInactive = "AccountInactive"

	ReasonObserveFailed = "ObserveFailed"

	commitQueue = 256
)

var GroupKind = schema.GroupKind{Group: v1alpha1.GroupName, Kind: Kind}

type Options struct {
	Bus *bus.Bus

	Network atprotorepo.RecordReader

	Pds pdsctl.Lookup

	Scope func(workspace string) bool

	Log *slog.Logger
}

type Controller struct {
	bus *bus.Bus

	network atprotorepo.RecordReader

	pds pdsctl.Lookup

	log *slog.Logger

	backends *binding.Table[backend]

	commits chan ctlruntime.Request

	mu sync.Mutex

	feeds map[string]*commitFeed

	ready map[binding.Target]bool

	closed bool

	relays sync.WaitGroup
}

type commitFeed struct {
	srv *atpds.Server

	stop chan struct{}
}

func New(opts Options) *Controller {
	log := opts.Log
	if log == nil {
		log = slog.New(slog.DiscardHandler)
	}
	return &Controller{bus: opts.Bus, network: opts.Network, pds: opts.Pds, log: log.With("component", "localrepo-controller"),
		backends: binding.New[backend](), commits: make(chan ctlruntime.Request, commitQueue), feeds: map[string]*commitFeed{},
		ready: map[binding.Target]bool{}}
}

func Register(m *ctlruntime.Manager, opts Options) (*Controller, error) {
	if err := m.AddToScheme(v1alpha1.AddToScheme); err != nil {
		return nil, err
	}
	ctl := New(opts)
	ctl.Provide()
	options := []ctlruntime.Option{ctlruntime.Export(Export),
		ctlruntime.Watches(func(_ context.Context, src ctlruntime.Request, obj *v1alpha1.Pds) []ctlruntime.Request {
			var out []ctlruntime.Request
			for _, t := range ctl.backends.Users(binding.Key(src.Workspace, PdsKind, obj.Name)) {
				out = append(out, ctlruntime.Request{Cluster: t.Cluster, Workspace: t.Workspace, Name: t.Name})
			}
			return out
		}),
		ctlruntime.Events(ctl.commits),
		ctlruntime.Gone(func(_ context.Context, req ctlruntime.Request) {
			ctl.release(binding.Target{Cluster: req.Cluster, Workspace: req.Workspace, Name: req.Name})
		})}
	if opts.Scope != nil {
		options = append(options, ctlruntime.InWorkspaces(opts.Scope))
	}
	if err := ctlruntime.For[*v1alpha1.LocalRepo](m, Name, ctl, options...); err != nil {
		return nil, fmt.Errorf("localrepo: registering the LocalRepo controller: %w", err)
	}
	if err := m.Go(Name+": commit feeds", ctl.serveFeeds); err != nil {
		return nil, fmt.Errorf("localrepo: the commit feeds: %w", err)
	}
	return ctl, nil
}

func (c *Controller) serveFeeds(ctx context.Context) error {
	<-ctx.Done()
	c.mu.Lock()
	c.closed = true
	for key, feed := range c.feeds {
		close(feed.stop)
		delete(c.feeds, key)
	}
	c.mu.Unlock()
	c.relays.Wait()
	return nil
}

func (c *Controller) Provide() {
	provide[atprotorepo.Repo](c, atprotorepo.CapabilityWrite)
	provide[atprotorepo.RecordWriter](c, atprotorepo.CapabilityWrite)
	provide[atprotorepo.RecordReader](c, atprotorepo.CapabilityRead)
	provide[atprotorepo.Subscriber](c, atprotorepo.CapabilitySubscribe)
}

func provide[T any](c *Controller, capability string) {
	bus.Provide(c.bus, GroupKind, capability, func(ctx context.Context, to commonv1.ObjectRef, scope []string) (T, error) {
		var zero T
		b, ok := c.backends.Get(to.Workspace, to.Name)
		if !ok {
			return zero, fmt.Errorf("%w: the LocalRepo %s is not reconciled", atprotorepo.ErrNotBound, to.String())
		}
		repo, err := open(ctx, c.bus, b, scope, c.network)
		if err != nil {
			return zero, err
		}
		out, ok := repo.(T)
		if !ok {
			return zero, fmt.Errorf("localrepo: the repo handle is not a %T", zero)
		}
		return out, nil
	})
}

func (c *Controller) Reconcile(ctx context.Context, req ctlruntime.Request, object *v1alpha1.LocalRepo) (ctlruntime.Result, error) {
	target := binding.Target{Cluster: req.Cluster, Workspace: req.Workspace, Name: req.Name}
	if !object.DeletionTimestamp.IsZero() {
		c.release(target)
		return ctlruntime.Result{}, nil
	}
	generation := object.Generation
	status := v1alpha1.AtprotoRepoBackendStatus{ObservedGeneration: generation, DID: strings.TrimSpace(object.Spec.DID),
		Conditions: slices.Clone(object.Status.Conditions)}
	pdsRef := object.Spec.PdsRef
	pdsRef.Group = v1alpha1.GroupName
	pdsRef.Workspace = pdsRef.InWorkspace(req.Workspace)
	switch {
	case !strings.EqualFold(strings.TrimSpace(pdsRef.Kind), PdsKind) || strings.TrimSpace(pdsRef.Name) == "":
		c.release(target)
		return c.notReady(ctx, req, object, status, ReasonInvalidSpec, "the pdsRef must name a Pds component", nil)
	case !validDID(status.DID):
		c.release(target)
		return c.notReady(ctx, req, object, status, ReasonInvalidSpec, fmt.Sprintf("the did %q is not a DID", status.DID), nil)
	}
	repo := backend{Self: commonv1.ObjectRef{Group: v1alpha1.GroupName, Kind: Kind, Name: object.Name, Workspace: req.Workspace},
		Labels: maps.Clone(object.Labels), Pds: pdsRef, DID: status.DID}
	pdsKey := binding.Key(pdsRef.Workspace, PdsKind, pdsRef.Name)
	prior, held := c.backends.Get(req.Workspace, req.Name)
	c.backends.Set(target, &repo, pdsKey)
	if !held || prior.Self != repo.Self || prior.Pds != repo.Pds || prior.DID != repo.DID || !maps.Equal(prior.Labels, repo.Labels) {
		c.bus.NotifyBound(bus.Key{Workspace: req.Workspace, Kind: Kind, Name: req.Name})
	}
	c.follow(pdsKey, pdsRef)
	c.prune()
	handle, err := accounts(ctx, c.bus, repo)
	if err != nil {
		return c.resolveFailed(ctx, req, object, status, err)
	}
	info, err := handle.Describe(ctx)
	if err != nil {
		return c.notReady(ctx, req, object, status, ReasonObserveFailed, err.Error(), err)
	}
	status.Address = info.URL
	account, err := handle.Account(ctx, status.DID)
	if errors.Is(err, pds.ErrNotFound) {
		return c.notReady(ctx, req, object, status, ReasonAccountMissing, err.Error(), nil)
	}
	if err != nil {
		return c.notReady(ctx, req, object, status, ReasonObserveFailed, err.Error(), err)
	}
	status.KeyFingerprint = account.SigningKey
	commits, err := syncer(ctx, c.bus, repo)
	if err != nil {
		return c.resolveFailed(ctx, req, object, status, err)
	}
	if head, err := commits.LatestCommit(ctx, status.DID); err == nil {
		status.Head, status.Rev = head.CID, head.Rev
	} else if !errors.Is(err, pds.ErrNotFound) {
		return c.notReady(ctx, req, object, status, ReasonObserveFailed, err.Error(), err)
	}
	if _, err := open(ctx, c.bus, repo, nil, nil); err != nil {
		return c.resolveFailed(ctx, req, object, status, err)
	}
	if !account.Active {
		return c.notReady(ctx, req, object, status, ReasonAccountInactive, "the Pds account "+status.DID+" is not active", nil)
	}
	status.Writable = account.SigningKey != ""
	conds.Set(&status.Conditions, generation, commonv1.ConditionReady, true, ReasonReady,
		fmt.Sprintf("the Pds %s signs the repo %s in process", pdsRef.Name, status.DID))
	if err := c.apply(ctx, req, object, status); err != nil {
		return ctlruntime.Result{}, err
	}
	c.settle(req, true)
	return ctlruntime.Result{}, nil
}

func (c *Controller) settle(req ctlruntime.Request, ready bool) {
	target := binding.Target{Cluster: req.Cluster, Workspace: req.Workspace, Name: req.Name}
	c.mu.Lock()
	was := c.ready[target]
	if ready {
		c.ready[target] = true
	} else {
		delete(c.ready, target)
	}
	c.mu.Unlock()
	if ready && !was {
		c.bus.NotifyBound(bus.Key{Workspace: req.Workspace, Kind: Kind, Name: req.Name})
	}
}

func (c *Controller) resolveFailed(ctx context.Context, req ctlruntime.Request, object *v1alpha1.LocalRepo,
	status v1alpha1.AtprotoRepoBackendStatus, err error) (ctlruntime.Result, error) {
	switch {
	case errors.Is(err, bus.ErrLinkDenied):
		return c.notReady(ctx, req, object, status, ReasonLinkDenied, err.Error(), err)
	case errors.Is(err, pds.ErrNotFound), errors.Is(err, atprotorepo.ErrNotFound), errors.Is(err, bus.ErrNoProvider):
		return c.notReady(ctx, req, object, status, ReasonPdsUnavailable, err.Error(), nil)
	}
	return c.notReady(ctx, req, object, status, ReasonObserveFailed, err.Error(), err)
}

func (c *Controller) notReady(ctx context.Context, req ctlruntime.Request, object *v1alpha1.LocalRepo, status v1alpha1.AtprotoRepoBackendStatus,
	reason, message string, cause error) (ctlruntime.Result, error) {
	c.settle(req, false)
	status.Writable = false
	conds.Set(&status.Conditions, object.Generation, commonv1.ConditionReady, false, reason, message)
	if err := c.apply(ctx, req, object, status); err != nil {
		return ctlruntime.Result{}, errors.Join(cause, err)
	}
	return ctlruntime.Result{}, cause
}

func (c *Controller) apply(ctx context.Context, req ctlruntime.Request, object *v1alpha1.LocalRepo, status v1alpha1.AtprotoRepoBackendStatus) error {
	next := object.DeepCopy()
	next.Status = status
	return req.ApplyStatus(ctx, next)
}

func validDID(raw string) bool {
	_, err := syntax.ParseDID(raw)
	return err == nil
}

type backend struct {
	Self commonv1.ObjectRef

	Labels map[string]string

	Pds commonv1.ObjectRef

	DID string
}

func (b backend) caller(ctx context.Context) context.Context {
	return bus.WithCallerLabels(ctx, b.Self, b.Labels)
}

func open(ctx context.Context, b *bus.Bus, repo backend, scope []string, network atprotorepo.RecordReader) (atprotorepo.Repo, error) {
	signer, err := bus.Resolve[atprotorepo.Repo](repo.caller(ctx), b, repo.Self, repo.Pds,
		bus.Capability{Name: pds.CapabilityAccount, Scope: []string{repo.DID}})
	if err != nil {
		return nil, err
	}
	return scoped.New(scoped.Route(signer, network), scope), nil
}

func accounts(ctx context.Context, b *bus.Bus, repo backend) (pds.Accounts, error) {
	return bus.Resolve[pds.Accounts](repo.caller(ctx), b, repo.Self, repo.Pds,
		bus.Capability{Name: pds.CapabilityAccount, Scope: []string{repo.DID}})
}

func syncer(ctx context.Context, b *bus.Bus, repo backend) (pds.Sync, error) {
	return bus.Resolve[pds.Sync](repo.caller(ctx), b, repo.Self, repo.Pds, bus.Capability{Name: pds.CapabilitySync})
}

func (c *Controller) release(target binding.Target) {
	c.backends.Set(target, nil)
	c.mu.Lock()
	delete(c.ready, target)
	c.mu.Unlock()
	c.prune()
}

func (c *Controller) follow(pdsKey string, pdsRef commonv1.ObjectRef) {
	if c.pds == nil {
		return
	}
	srv, ok := c.pds(pdsRef.Workspace, strings.TrimSpace(pdsRef.Name))
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.closed {
		return
	}
	current := c.feeds[pdsKey]
	if !ok || srv == nil {
		if current != nil {
			close(current.stop)
			delete(c.feeds, pdsKey)
		}
		return
	}
	if current != nil && current.srv == srv {
		return
	}
	if current != nil {
		close(current.stop)
	}
	feed := &commitFeed{srv: srv, stop: make(chan struct{})}
	c.feeds[pdsKey] = feed
	c.relays.Go(func() { c.relay(pdsKey, feed) })
}

func (c *Controller) prune() {
	c.mu.Lock()
	defer c.mu.Unlock()
	for key, feed := range c.feeds {
		if len(c.backends.Users(key)) == 0 {
			close(feed.stop)
			delete(c.feeds, key)
		}
	}
}

func (c *Controller) relay(pdsKey string, feed *commitFeed) {
	for {
		live, cancel := feed.srv.Sequencer().Subscribe()
		stopped := c.forward(pdsKey, feed, live)
		cancel()
		if stopped {
			return
		}
		c.log.Warn("the Pds commit feed dropped the LocalRepo controller; it resubscribes", "pds", pdsKey)
		if !c.enqueue(pdsKey, "", feed.stop) {
			return
		}
	}
}

func (c *Controller) forward(pdsKey string, feed *commitFeed, live <-chan *atrepo.CommitEvent) bool {
	for {
		select {
		case <-feed.stop:
			return true
		case ev, ok := <-live:
			if !ok {
				return false
			}
			if !c.enqueue(pdsKey, ev.Repo.String(), feed.stop) {
				return true
			}
		}
	}
}

func (c *Controller) enqueue(pdsKey, did string, stop <-chan struct{}) bool {
	for _, t := range c.backends.Users(pdsKey) {
		if did != "" {
			b, ok := c.backends.Get(t.Workspace, t.Name)
			if !ok || b.DID != did {
				continue
			}
		}
		select {
		case <-stop:
			return false
		case c.commits <- ctlruntime.Request{Cluster: t.Cluster, Name: t.Name}:
		}
	}
	return true
}