~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
236 lines · 8.7 KB Raw History
package componentlink

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

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1"
	exposurev1alpha1 "github.com/johnandersen777/socialweb-computer-kcp/api/exposure/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/bus"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/component"
	ctlruntime "github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/exposure"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/binding"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/conds"
)

const (
	Name = "componentlink"

	Export = "exposures"
)

type Options struct {
	Policy *exposure.LinkPolicy

	Registry *component.Registry

	Providers *bus.Bus

	Scope func(workspace string) bool

	Log *slog.Logger
}

type Controller struct {
	policy *exposure.LinkPolicy

	registry *component.Registry

	providers *bus.Bus

	log *slog.Logger

	targets *binding.Table[struct{}]

	events *ctlruntime.Queue
}

func New(opts Options) (*Controller, error) {
	if opts.Policy == nil {
		return nil, errors.New("componentlink: a link policy is required")
	}
	log := opts.Log
	if log == nil {
		log = slog.New(slog.DiscardHandler)
	}
	return &Controller{policy: opts.Policy, registry: opts.Registry, providers: opts.Providers, log: log.With("component", "componentlink-controller"),
		targets: binding.New[struct{}](), events: ctlruntime.NewQueue()}, nil
}

func Register(m *ctlruntime.Manager, opts Options) (*Controller, error) {
	if err := m.AddToScheme(exposurev1alpha1.AddToScheme); err != nil {
		return nil, err
	}
	ctl, err := New(opts)
	if err != nil {
		return nil, err
	}
	events, err := ctl.events.Run(m, Name+": components")
	if err != nil {
		return nil, fmt.Errorf("%s: forwarding component changes: %w", Name, err)
	}
	options := []ctlruntime.Option{ctlruntime.Export(Export), ctlruntime.Events(events), ctlruntime.Gone(ctl.gone)}
	if opts.Scope != nil {
		options = append(options, ctlruntime.InWorkspaces(opts.Scope))
	}
	if err := ctlruntime.For[*exposurev1alpha1.ComponentLink](m, Name, ctl, options...); err != nil {
		return nil, fmt.Errorf("componentlink: registering the ComponentLink controller: %w", err)
	}
	return ctl, nil
}

func (c *Controller) ComponentChanged(key component.Key) {
	for _, t := range c.targets.Users(binding.Key(key.Workspace, key.Kind, key.Name)) {
		c.events.Add(ctlruntime.Request{Cluster: t.Cluster, Workspace: t.Workspace, Name: t.Name})
	}
}

func (c *Controller) gone(_ context.Context, req ctlruntime.Request) {
	c.forget(binding.Target{Cluster: req.Cluster, Workspace: req.Workspace, Name: req.Name})
}

func (c *Controller) forget(self binding.Target) {
	c.policy.Remove(self.Workspace, self.Name)
	c.targets.Set(self, nil)
}

func (c *Controller) Reconcile(ctx context.Context, req ctlruntime.Request, object *exposurev1alpha1.ComponentLink) (ctlruntime.Result, error) {
	self := binding.Target{Cluster: req.Cluster, Workspace: req.Workspace, Name: req.Name}
	if !object.DeletionTimestamp.IsZero() {
		c.forget(self)
		return ctlruntime.Result{}, nil
	}
	generation := object.Generation
	status := object.Status
	status.ObservedGeneration = generation
	compiled, reason, err := compileLink(req.Workspace, req.Name, object.Spec)
	switch {
	case err != nil:
		c.policy.Remove(req.Workspace, req.Name)
		conds.Set(&status.Conditions, generation, exposurev1alpha1.ComponentLinkConditionAccepted, false, reason, err.Error())
	case object.Spec.Suspend:
		c.policy.Remove(req.Workspace, req.Name)
		conds.Set(&status.Conditions, generation, exposurev1alpha1.ComponentLinkConditionAccepted, false, "Suspended", "the ComponentLink is suspended")
	default:
		c.policy.Set(req.Workspace, req.Name, []bus.Link{compiled})
		conds.Set(&status.Conditions, generation, exposurev1alpha1.ComponentLinkConditionAccepted, true, "Accepted",
			fmt.Sprintf("the bus allows %s to call %s with %s", fromString(compiled.From), compiled.To.String(), capabilitiesString(compiled.Capabilities)))
	}
	to := linkTarget(req.Workspace, object.Spec.To)
	if to.Kind != "" && to.Name != "" {
		c.targets.Set(self, &struct{}{}, binding.Key(to.Workspace, to.Kind, to.Name))
	} else {
		c.targets.Set(self, nil)
	}
	if c.targetRegistered(to) {
		conds.Set(&status.Conditions, generation, exposurev1alpha1.ComponentLinkConditionResolvedRefs, true, "TargetRegistered",
			fmt.Sprintf("the target %s is registered", to.String()))
	} else {
		conds.Set(&status.Conditions, generation, exposurev1alpha1.ComponentLinkConditionResolvedRefs, false, "TargetMissing",
			fmt.Sprintf("the target %s is not registered", to.String()))
	}
	next := object.DeepCopy()
	next.Status = status
	return ctlruntime.Result{}, req.ApplyStatus(ctx, next)
}

func (c *Controller) targetRegistered(to commonv1.ObjectRef) bool {
	if to.Kind == "" || to.Name == "" {
		return false
	}
	if c.registry != nil && c.registry.Has(to.Workspace, to.Kind, to.Name) {
		return true
	}
	return c.providers.Provides(to.Group, to.Kind)
}

func linkTarget(workspace string, to commonv1.ObjectRef) commonv1.ObjectRef {
	return commonv1.ObjectRef{Group: strings.TrimSpace(to.Group), Kind: strings.TrimSpace(to.Kind), Name: strings.TrimSpace(to.Name),
		Workspace: cmp.Or(strings.TrimSpace(to.Workspace), workspace)}
}

func compileLink(workspace, name string, spec exposurev1alpha1.ComponentLinkSpec) (bus.Link, string, error) {
	to := linkTarget(workspace, spec.To)
	switch {
	case to.Kind == "" || to.Name == "":
		return bus.Link{}, "InvalidSpec", errors.New("the ComponentLink to ref needs a kind and a name")
	case to.Workspace != workspace:
		return bus.Link{}, "NotProducerWorkspace", fmt.Errorf("the ComponentLink must live in the workspace of its target %s", to.String())
	}
	from, err := compileFrom(workspace, spec.From)
	if err != nil {
		return bus.Link{}, "InvalidSpec", err
	}
	if len(spec.Capabilities) == 0 {
		return bus.Link{}, "InvalidSpec", errors.New("the ComponentLink grants no capability")
	}
	capabilities := make([]bus.Capability, 0, len(spec.Capabilities))
	for _, capability := range spec.Capabilities {
		capabilityName := strings.ToLower(strings.TrimSpace(capability.Name))
		if capabilityName == "" {
			return bus.Link{}, "InvalidSpec", errors.New("the ComponentLink capabilities list has an empty name")
		}
		scope := make([]string, 0, len(capability.Scope))
		for _, entry := range capability.Scope {
			if entry = strings.TrimSpace(entry); entry == "" {
				return bus.Link{}, "InvalidSpec", fmt.Errorf("the capability %s has an empty scope entry", capabilityName)
			}
			scope = append(scope, entry)
		}
		capabilities = append(capabilities, bus.Capability{Name: capabilityName, Scope: scope})
	}
	return bus.Link{Workspace: workspace, Name: name, From: from, To: to, Capabilities: capabilities}, "", nil
}

func compileFrom(workspace string, from exposurev1alpha1.ComponentLinkFrom) (bus.LinkFrom, error) {
	switch {
	case from.Ref != nil && from.Selector != nil:
		return bus.LinkFrom{}, errors.New("the ComponentLink from names both a ref and a selector")
	case from.Ref != nil:
		ref := linkTarget(workspace, *from.Ref)
		if ref.Kind == "" || ref.Name == "" {
			return bus.LinkFrom{}, errors.New("the ComponentLink from ref needs a kind and a name")
		}
		return bus.LinkFrom{Ref: &ref}, nil
	case from.Selector != nil:
		sel := from.Selector
		if strings.TrimSpace(sel.Kind) == "" {
			return bus.LinkFrom{}, errors.New("the ComponentLink from selector needs a kind")
		}
		requirements := make([]metav1.LabelSelectorRequirement, 0, len(sel.MatchExpressions))
		for _, r := range sel.MatchExpressions {
			requirements = append(requirements, metav1.LabelSelectorRequirement{Key: r.Key, Operator: metav1.LabelSelectorOperator(r.Operator),
				Values: append([]string(nil), r.Values...)})
		}
		selector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: sel.MatchLabels, MatchExpressions: requirements})
		if err != nil {
			return bus.LinkFrom{}, fmt.Errorf("the ComponentLink from selector: %w", err)
		}
		return bus.LinkFrom{Group: strings.TrimSpace(sel.Group), Kind: strings.TrimSpace(sel.Kind),
			Workspace: cmp.Or(strings.TrimSpace(sel.Workspace), workspace), Selector: selector}, nil
	default:
		return bus.LinkFrom{}, errors.New("the ComponentLink from needs a ref or a selector")
	}
}

func fromString(from bus.LinkFrom) string {
	if from.Ref != nil {
		return from.Ref.String()
	}
	return from.Workspace + ":" + from.Kind + "{" + from.Selector.String() + "}"
}

func capabilitiesString(capabilities []bus.Capability) string {
	parts := make([]string, 0, len(capabilities))
	for _, capability := range capabilities {
		parts = append(parts, capability.String())
	}
	return strings.Join(parts, ", ")
}