~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
358 lines · 12.1 KB Raw History
package compose

import (
	"context"
	"fmt"
	"reflect"
	"sort"
	"strings"

	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"

	exposurev1alpha1 "github.com/johnandersen777/socialweb-computer-kcp/api/exposure/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime/kube"
)

const (
	ConditionComponentsReady         = "ComponentsReady"
	ConditionComponentSuspended      = "ComponentSuspended"
	ConditionComponentOverridesValid = "ComponentOverridesValid"
	ConditionLimitsEnforced          = "LimitsEnforced"
	ConditionSuspended               = "Suspended"

	ReasonSuspended               = "Suspended"
	ReasonNotSuspended            = "NotSuspended"
	ReasonOverridesApplied        = "OverridesApplied"
	ReasonComponentLimitsInactive = "ComponentLimitsNotEnforced"
	ReasonParticipationSuspended  = "ParticipationSuspended"
	ReasonActive                  = "Active"

	SuspendedMessage = "suspended: no new participation; obligations already taken (running VMs, StateSync acks, offer withdrawal, credential renewal) continue"

	SuspendingMessage = "suspending: waiting for every component to apply the suspend"
)

var componentOwnedStatus = []string{"market"}

type Snapshot struct {
	Views      []ComponentView
	Components []v1alpha1.ApplicationComponent
	Listeners  []v1alpha1.ApplicationListener
}

func (c *Composer) Snapshot(ctx context.Context, ws, owner string) (Snapshot, error) {
	views, err := c.ListComponents(ctx, ws, owner)
	if err != nil {
		return Snapshot{}, err
	}
	snap := Snapshot{Views: views}
	for _, v := range views {
		if !IsExposureKind(v.Kind) {
			snap.Components = append(snap.Components, v.ApplicationComponent())
		}
		snap.Listeners = append(snap.Listeners, v.Listeners()...)
	}
	sort.Slice(snap.Listeners, func(i, j int) bool {
		if snap.Listeners[i].Component != snap.Listeners[j].Component {
			return snap.Listeners[i].Component < snap.Listeners[j].Component
		}
		return snap.Listeners[i].Name < snap.Listeners[j].Name
	})
	return snap, nil
}

func (s Snapshot) View(kind, name string) (ComponentView, bool) {
	for _, v := range s.Views {
		if v.Kind == kind && v.Name == name {
			return v, true
		}
	}
	return ComponentView{}, false
}

func (s Snapshot) Ready(kind, name string) bool {
	v, ok := s.View(kind, name)
	return ok && v.Status.Ready
}

func (s Snapshot) ExposureURL(name string) (string, bool) {
	v, ok := s.View(ComponentKindExposure, name)
	if !ok || !v.Status.Ready {
		return "", false
	}
	for _, e := range v.Status.Endpoints {
		if e.URL != "" {
			return e.URL, true
		}
	}
	return "", false
}

func DesiredSuspended(d Desired) bool {
	m, err := SpecMap(d.Spec)
	if err != nil || m == nil {
		return false
	}
	v, _ := m["suspend"].(bool)
	return v
}

func ParticipationSuspended(d Desired) bool {
	m, err := SpecMap(d.Spec)
	if err != nil || m == nil {
		return false
	}
	v, _ := m["suspendParticipation"].(bool)
	return v
}

func (s Snapshot) Satisfied(desired []Desired) bool {
	if len(desired) == 0 {
		return false
	}
	for _, d := range desired {
		if IsExposureKind(d.Kind) {
			continue
		}
		v, ok := s.View(d.Kind, d.Name)
		if !ok {
			return false
		}
		if v.Status.ObservedGeneration != 0 && v.Status.ObservedGeneration < v.Generation {
			return false
		}
		if DesiredSuspended(d) {
			if v.Status.Phase != v1alpha1.ComponentPhaseSuspended {
				return false
			}
			continue
		}
		if !v.Status.Ready {
			return false
		}
	}
	return true
}

func Ready(st v1alpha1.ApplicationStatus, msg string) v1alpha1.ApplicationStatus {
	st.Phase, st.Message = v1alpha1.ApplicationPhaseReady, msg
	return st
}

func Suspended(st v1alpha1.ApplicationStatus) v1alpha1.ApplicationStatus {
	st.Phase, st.Message = v1alpha1.ApplicationPhaseSuspended, SuspendedMessage
	return st
}

func SuspendedOnceApplied(st v1alpha1.ApplicationStatus, snap Snapshot, held []Desired) v1alpha1.ApplicationStatus {
	if len(held) == 0 || snap.Satisfied(held) {
		return Suspended(st)
	}
	st.Phase, st.Message = v1alpha1.ApplicationPhaseProvisioning, SuspendingMessage
	return st
}

func SetSuspendCondition(conditions *[]metav1.Condition, gen int64, suspend bool) {
	if suspend {
		meta.SetStatusCondition(conditions, metav1.Condition{Type: ConditionSuspended, Status: metav1.ConditionTrue,
			Reason: ReasonParticipationSuspended, Message: SuspendedMessage, ObservedGeneration: gen})
		return
	}
	meta.SetStatusCondition(conditions, metav1.Condition{Type: ConditionSuspended, Status: metav1.ConditionFalse,
		Reason: ReasonActive, Message: "the root participates", ObservedGeneration: gen})
}

func FillComponentStatus(conditions *[]metav1.Condition, summaryOut **v1alpha1.ComponentSummary,
	overrideStatusOut *[]v1alpha1.ComponentOverrideStatus, gen int64, rootSuspend bool, overrides v1alpha1.ComponentOverrides,
	desired []Desired, snap Snapshot, overrideErr error) {
	summary := &v1alpha1.ComponentSummary{}
	var suspended, notReady []string
	for _, d := range desired {
		if IsExposureKind(d.Kind) {
			continue
		}
		summary.Desired++
		key := ComponentKey(d.Kind, d.Name)
		v, ok := snap.View(d.Kind, d.Name)
		if !ok {
			summary.NotReady++
			notReady = append(notReady, key)
			continue
		}
		if DesiredSuspended(d) || (ParticipationSuspended(d) && v.Status.Ready) {
			summary.Suspended++
			suspended = append(suspended, key)
			continue
		}
		if v.Status.Ready {
			summary.Ready++
		} else {
			summary.NotReady++
			notReady = append(notReady, key)
		}
	}
	*summaryOut = summary
	if summary.NotReady == 0 && summary.Desired > 0 {
		meta.SetStatusCondition(conditions, metav1.Condition{Type: ConditionComponentsReady, Status: metav1.ConditionTrue,
			Reason: "Ready", Message: fmt.Sprintf("%d component(s) are ready or suspended by intent", summary.Desired), ObservedGeneration: gen})
	} else {
		meta.SetStatusCondition(conditions, metav1.Condition{Type: ConditionComponentsReady, Status: metav1.ConditionFalse,
			Reason: "Waiting", Message: strings.Join(notReady, "; "), ObservedGeneration: gen})
	}
	if len(suspended) > 0 {
		meta.SetStatusCondition(conditions, metav1.Condition{Type: ConditionComponentSuspended, Status: metav1.ConditionTrue,
			Reason: ReasonSuspended, Message: strings.Join(suspended, "; "), ObservedGeneration: gen})
	} else {
		meta.SetStatusCondition(conditions, metav1.Condition{Type: ConditionComponentSuspended, Status: metav1.ConditionFalse,
			Reason: ReasonNotSuspended, Message: "no component is suspended by intent", ObservedGeneration: gen})
	}
	SetSuspendCondition(conditions, gen, rootSuspend)
	if overrideErr != nil {
		meta.SetStatusCondition(conditions, metav1.Condition{Type: ConditionComponentOverridesValid, Status: metav1.ConditionFalse,
			Reason: OverrideErrorReason(overrideErr), Message: overrideErr.Error(), ObservedGeneration: gen})
	} else {
		meta.SetStatusCondition(conditions, metav1.Condition{Type: ConditionComponentOverridesValid, Status: metav1.ConditionTrue,
			Reason: ReasonOverridesApplied, Message: fmt.Sprintf("%d component override(s) resolved", len(overrides)), ObservedGeneration: gen})
	}
	var limited []string
	for key, ov := range overrides {
		if ov.Limits != nil {
			limited = append(limited, key)
		}
	}
	if len(limited) > 0 {
		sort.Strings(limited)
		meta.SetStatusCondition(conditions, metav1.Condition{Type: ConditionLimitsEnforced, Status: metav1.ConditionFalse,
			Reason: ReasonComponentLimitsInactive, Message: "the in-process component runtime does not enforce ComponentLimits for " + strings.Join(limited, "; "),
			ObservedGeneration: gen})
	}
	resolved, err := resolveOverrides(overrides, desired)
	if err == nil {
		keys := make([]string, 0, len(resolved))
		for key := range resolved {
			keys = append(keys, key)
		}
		sort.Strings(keys)
		out := make([]v1alpha1.ComponentOverrideStatus, 0, len(keys))
		for _, key := range keys {
			ov := resolved[key]
			kind, name, _ := strings.Cut(key, "/")
			item := v1alpha1.ComponentOverrideStatus{Component: key, RestartPolicy: string(ov.RestartPolicy),
				Suspend: ov.Suspend != nil && *ov.Suspend}
			if v, ok := snap.View(kind, name); ok {
				item.Generation = v.Generation
				item.Applied = true
			}
			out = append(out, item)
		}
		*overrideStatusOut = out
	}
}

func FillApplicationStatus(st *v1alpha1.ApplicationStatus, gen int64, rootSuspend bool, overrides v1alpha1.ComponentOverrides,
	desired []Desired, snap Snapshot, overrideErr error) {
	FillComponentStatus(&st.Conditions, &st.ComponentSummary, &st.ComponentOverrides, gen, rootSuspend, overrides, desired, snap, overrideErr)
}

func SetOverrideErrorIfAny(conditions *[]metav1.Condition, summary **v1alpha1.ComponentSummary, gen int64, err error) {
	if _, ok := err.(*OverrideError); ok {
		meta.SetStatusCondition(conditions, metav1.Condition{Type: ConditionComponentOverridesValid, Status: metav1.ConditionFalse,
			Reason: OverrideErrorReason(err), Message: err.Error(), ObservedGeneration: gen})
		*summary = &v1alpha1.ComponentSummary{}
	}
}

func NewStatus(gen int64, profile string, previous v1alpha1.ApplicationStatus) v1alpha1.ApplicationStatus {
	return v1alpha1.ApplicationStatus{ObservedGeneration: gen, Profile: profile, Did: previous.Did, AppliedSet: previous.AppliedSet,
		Conditions: append([]metav1.Condition(nil), previous.Conditions...)}
}

func (c *Composer) WriteStatus(ctx context.Context, r Root, gvr schema.GroupVersionResource, status any) error {
	m, err := ToMap(status)
	if err != nil {
		return err
	}
	for _, k := range componentOwnedStatus {
		delete(m, k)
	}
	c.samples.into(ctx, c, m, jsonFields(status))
	if _, ok := m["conditions"]; ok {
		gen, _ := m["observedGeneration"].(int64)
		if f, ok := m["observedGeneration"].(float64); ok {
			gen = int64(f)
		}
		withDriftCondition(m, c.driftCondition(r, gen))
	}
	u := NewDoc(r.APIVersion, r.Kind, r.Name)
	u["status"] = m
	ri, err := c.Client(r.Workspace, gvr)
	if err != nil {
		return err
	}
	if _, err := kube.Apply[kube.Doc](ctx, ri, u, kube.ApplyOptions{FieldManager: r.Manager(), Force: true, Status: true}); err != nil {
		return fmt.Errorf("compose: applying the status of %s %s: %w", r.Kind, r.Name, err)
	}
	return nil
}

func jsonFields(v any) map[string]bool {
	t := reflect.TypeOf(v)
	for t != nil && t.Kind() == reflect.Pointer {
		t = t.Elem()
	}
	out := map[string]bool{}
	if t == nil || t.Kind() != reflect.Struct {
		return out
	}
	for i := range t.NumField() {
		name, _, _ := strings.Cut(t.Field(i).Tag.Get("json"), ",")
		if name != "" && name != "-" {
			out[name] = true
		}
	}
	return out
}

func SetIngressRegisteredCondition(conditions *[]metav1.Condition, gen int64, snap Snapshot, exposureName string,
	mode v1alpha1.MarketCommsMode, fallback v1alpha1.SecretsFallback) string {
	v, ok := snap.View(ComponentKindExposure, exposureName)
	var registered *metav1.Condition
	if ok {
		registered = meta.FindStatusCondition(v.Status.Conditions, exposurev1alpha1.ConditionIngressRegistered)
	}
	if registered == nil {
		meta.RemoveStatusCondition(conditions, exposurev1alpha1.ConditionIngressRegistered)
		return ""
	}
	if registered.Status == metav1.ConditionTrue {
		meta.SetStatusCondition(conditions, metav1.Condition{Type: exposurev1alpha1.ConditionIngressRegistered, Status: metav1.ConditionTrue,
			Reason: registered.Reason, Message: "the Exposure " + exposureName + ": " + registered.Message, ObservedGeneration: gen})
		return ""
	}
	down := "ingress down (" + registered.Reason + ")"
	switch mode {
	case v1alpha1.MarketCommsModeBoth, v1alpha1.MarketCommsModeFirehose:
		down += "; bids and events arrive over streams"
	default:
		down += "; bids and events wait for the ingress to register"
	}
	if fallback == v1alpha1.SecretsFallbackSealed {
		down += "; guest secrets use the sealed fallback"
	}
	meta.SetStatusCondition(conditions, metav1.Condition{Type: exposurev1alpha1.ConditionIngressRegistered, Status: metav1.ConditionFalse,
		Reason: registered.Reason, Message: down + "; the Exposure " + exposureName + ": " + registered.Message, ObservedGeneration: gen})
	return down
}

func ApplicationStatusOf(u *Generic) (v1alpha1.ApplicationStatus, error) {
	var st v1alpha1.ApplicationStatus
	m := u.Status
	if len(m) == 0 {
		return st, nil
	}
	return st, runtime.DefaultUnstructuredConverter.FromUnstructured(m, &st)
}