~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
277 lines · 8.4 KB Raw History
package childapplication

import (
	"cmp"
	"context"
	"fmt"
	"regexp"
	"sort"
	"strings"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	ctlruntime "github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/conds"
)

var dnsLabel = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)

type observed struct {
	workspace string

	watched []string

	instance string

	workspaceExists bool

	workspaceReady bool

	workspaceDeleting bool

	coreExpected bool

	coreFound bool

	coreRef string

	coreReady bool

	coreFailed bool

	components []v1alpha1.ChildApplicationComponent

	componentsReady bool

	componentFailed bool

	stopped bool

	deleting bool

	invalid string

	orphaned bool

	orphanReason string

	orphanMessage string
}

func (c *Controller) Reconcile(ctx context.Context, req ctlruntime.Request, ca *v1alpha1.ChildApplication) (ctlruntime.Result, error) {
	o, err := c.observe(ctx, req, ca)
	c.index(req, refs{network: networkKey{cluster: req.Cluster, name: ca.Spec.NetworkRef.Name}, instance: o.instance, workspaces: o.watched})
	if err != nil {
		return ctlruntime.Result{}, err
	}
	o.deleting = !ca.DeletionTimestamp.IsZero()
	next := ca.DeepCopy()
	next.Status = statusOf(ca, o)
	if err := req.ApplyStatus(ctx, next); err != nil {
		return ctlruntime.Result{}, err
	}
	return ctlruntime.Result{Waiting: o.deleting && o.workspaceExists && o.orphaned}, nil
}

func (c *Controller) observe(ctx context.Context, req ctlruntime.Request, ca *v1alpha1.ChildApplication) (observed, error) {
	var o observed
	if ca.Spec.NetworkRef.Name == "" {
		o.invalid = "spec.network is required"
		return o, nil
	}
	if ca.Spec.App == "" {
		o.invalid = "spec.app is required"
		return o, nil
	}
	if len(ca.Spec.App) > 63 || !dnsLabel.MatchString(ca.Spec.App) {
		o.invalid = fmt.Sprintf("spec.app %q is not a DNS label", ca.Spec.App)
		return o, nil
	}
	placement := ""
	if ca.Spec.Placement != "" {
		if !logicalCluster(ca.Spec.Placement) {
			o.invalid = fmt.Sprintf("spec.placement %q is not a logical cluster path", ca.Spec.Placement)
			return o, nil
		}
		if strings.Contains(ca.Spec.Placement, ":") {
			placement = ca.Spec.Placement
		}
	}

	var appStatus *v1alpha1.NetworkAppStatus
	var network v1alpha1.MarketNetwork
	err := req.Get(ctx, "", ca.Spec.NetworkRef.Name, &network)
	switch {
	case apierrors.IsNotFound(err):
		o.orphaned = true
		o.orphanReason = "NetworkMissing"
		o.orphanMessage = fmt.Sprintf("the MarketNetwork %s does not exist", ca.Spec.NetworkRef.Name)
	case err != nil:
		return o, err
	default:
		listed := false
		for i := range network.Spec.Apps {
			if network.Spec.Apps[i].Name == ca.Spec.App {
				listed = true
				o.stopped = network.Spec.Apps[i].Stopped
				break
			}
		}
		for i := range network.Status.Apps {
			if network.Status.Apps[i].Name == ca.Spec.App {
				appStatus = &network.Status.Apps[i]
				break
			}
		}
		if !listed {
			o.orphaned = true
			o.orphanReason = "AppMissing"
			o.orphanMessage = fmt.Sprintf("the MarketNetwork %s lists no application %s", ca.Spec.NetworkRef.Name, ca.Spec.App)
		}
	}

	workspace := placement
	if appStatus != nil && appStatus.Workspace != "" {
		workspace = appStatus.Workspace
	}
	if appStatus != nil {
		o.instance = appStatus.Instance
	}
	workspace = cmp.Or(workspace, req.Workspace+":"+ca.Spec.App)
	if !logicalCluster(workspace) {
		o.invalid = fmt.Sprintf("the child workspace %q is not a logical cluster path", workspace)
		return o, nil
	}
	o.workspace = workspace
	o.watched = []string{workspace}
	parent, name, ok := splitCluster(workspace)
	if !ok {
		o.invalid = fmt.Sprintf("the child workspace %q is not a logical cluster path", workspace)
		return o, nil
	}
	ws, found, err := c.workspaces.Get(ctx, parent, name)
	if err != nil {
		return o, err
	}
	if !found {
		ws, found, err = c.workspaces.ByLabel(ctx, parent, AppWorkspaceLabel, ca.Spec.App)
		if err != nil && !absent(err) {
			return o, err
		}
		if found {
			o.workspace = parent + ":" + ws.Name
			o.watched = append(o.watched, o.workspace)
		}
	}
	if found {
		o.workspaceExists = true
		o.workspaceDeleting = ws.Deleting
		o.workspaceReady = ws.Phase == "Ready" && !ws.Deleting
	}

	if o.instance != "" {
		o.coreExpected = true
		ref, ready, failed, err := c.observeCore(ctx, o.instance)
		if err != nil {
			return o, err
		}
		o.coreRef, o.coreReady, o.coreFailed = ref, ready, failed
		o.coreFound = ref != ""
	}

	if o.workspaceExists && o.workspaceReady {
		components, failed, ready, err := c.observeComponents(ctx, o.workspace)
		if err != nil {
			if appStatus == nil {
				return o, err
			}
			components, failed, ready = fallbackComponents(appStatus)
		}
		o.components, o.componentFailed, o.componentsReady = components, failed, ready
	} else if appStatus != nil {
		o.components, o.componentFailed, o.componentsReady = fallbackComponents(appStatus)
	}
	return o, nil
}

func absent(err error) bool {
	return apierrors.IsNotFound(err) || apierrors.IsForbidden(err) || meta.IsNoMatchError(err)
}

func statusOf(ca *v1alpha1.ChildApplication, o observed) v1alpha1.ChildApplicationStatus {
	generation := ca.Generation
	status := v1alpha1.ChildApplicationStatus{ObservedGeneration: generation, Workspace: o.workspace, CoreRef: o.coreRef,
		Components: append([]v1alpha1.ChildApplicationComponent(nil), o.components...)}
	status.Conditions = append([]metav1.Condition(nil), ca.Status.Conditions...)
	sortComponents(status.Components)
	if len(status.Components) == 0 {
		status.Components = nil
	}

	ready, progressing, failed, orphaned := false, false, false, false
	reason, message := "Reconciling", "the child application is being observed"
	switch {
	case o.deleting && o.orphaned && o.workspaceExists:
		orphaned, reason, message = true, "WorkspaceKept", "the child workspace is kept for its MarketNetwork; remove it there"
	case o.deleting:
		progressing, reason, message = true, "Deleting", "the child workspace belongs to its MarketNetwork and is not deleted"
	case o.invalid != "":
		failed, reason, message = true, "InvalidSpec", o.invalid
	case o.orphaned:
		orphaned, reason, message = true, o.orphanReason, o.orphanMessage
	case o.stopped:
		progressing, reason, message = true, "Stopped", "the MarketNetwork lists this application as stopped"
	case !o.workspaceExists:
		progressing, reason, message = true, "WaitingForWorkspace", "the child workspace does not exist yet"
	case o.workspaceDeleting:
		progressing, reason, message = true, "WorkspaceDeleting", "the child workspace is being deleted"
	case !o.workspaceReady:
		progressing, reason, message = true, "WorkspaceNotReady", "the child workspace is not Ready yet"
	case o.coreFailed:
		failed, reason, message = true, "CoreFailed", "the child core HostProcess failed"
	case o.coreExpected && !o.coreFound:
		progressing, reason, message = true, "WaitingForCore", "the child core HostProcess is not placed yet"
	case o.coreFound && !o.coreReady:
		progressing, reason, message = true, "CoreNotReady", "the child core HostProcess is not Running yet"
	case o.componentFailed:
		failed, reason, message = true, "ComponentFailed", "a child component failed"
	case !o.componentsReady:
		progressing, reason, message = true, "WaitingForComponents", "the child components are not all Ready"
	default:
		ready, reason, message = true, "Ready", "the child workspace, core and components are Ready"
	}

	conds.Set(&status.Conditions, generation, ConditionReady, ready, reason, message)
	conds.Set(&status.Conditions, generation, ConditionProgressing, progressing, reason, message)
	conds.Set(&status.Conditions, generation, ConditionFailed, failed, reason, message)
	conds.Set(&status.Conditions, generation, ConditionOrphaned, orphaned, reason, message)
	sort.Slice(status.Conditions, func(i, j int) bool { return status.Conditions[i].Type < status.Conditions[j].Type })
	return status
}

func logicalCluster(s string) bool {
	if s == "" {
		return false
	}
	for segment := range strings.SplitSeq(s, ":") {
		if len(segment) > 63 || !dnsLabel.MatchString(segment) {
			return false
		}
	}
	return true
}

func splitCluster(s string) (string, string, bool) {
	i := strings.LastIndex(s, ":")
	if i <= 0 || i == len(s)-1 {
		return "", "", false
	}
	parent, name := s[:i], s[i+1:]
	if !logicalCluster(parent) || len(name) > 63 || !dnsLabel.MatchString(name) {
		return "", "", false
	}
	return parent, name, true
}