~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
298 lines · 11.4 KB Raw History
package computevm

import (
	"context"
	"fmt"
	"log/slog"
	"reflect"
	"time"

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

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

	commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1"
)

const (
	Name = "computevm"

	Export = "computevms"

	Workers = 4
)

type Controller struct {
	host *computevm.Host

	handle string

	log *slog.Logger
}

func Register(m *ctlruntime.Manager, host *computevm.Host) (*Controller, error) {
	handle := host.Config().Handle
	ctl := &Controller{host: host, handle: handle, log: host.Log().With("controller", Name)}
	if err := ctlruntime.For[*v1alpha1.ComputeVM](m, Name, ctl, ctlruntime.Export(Export),
		ctlruntime.FieldManager(v1alpha1.FieldManagerComputeVMController), ctlruntime.Finalizer(v1alpha1.FinalizerComputeVM),
		ctlruntime.SharedStatus(), ctlruntime.Workers(Workers), ctlruntime.Owns[*v1alpha1.FirecrackerVM](),
		ctlruntime.Only(func(vm *v1alpha1.ComputeVM) bool { return vm.Spec.Owner.Handle == handle })); err != nil {
		return nil, fmt.Errorf("computevm: registering the ComputeVM controller: %w", err)
	}
	return ctl, nil
}

func setCondition(st *v1alpha1.ComputeVMStatus, gen int64, t string, ok bool, reason, msg string) {
	conds.Set(&st.Conditions, gen, t, ok, reason, computevm.CapMessage(msg))
}

func (c *Controller) backend(ctx context.Context, req ctlruntime.Request, vm *v1alpha1.ComputeVM) (*v1alpha1.FirecrackerVM, error) {
	var fvm v1alpha1.FirecrackerVM
	err := req.Get(ctx, "", vm.Spec.BackendRef.Name, &fvm)
	if apierrors.IsNotFound(err) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return &fvm, nil
}

func isSandbox(vm *v1alpha1.ComputeVM) bool {
	ref := vm.Spec.BackendRef
	return ref.Kind == v1alpha1.BackendKindDenoSandbox && (ref.Group == "" || ref.Group == v1alpha1.GroupName) && ref.Name != ""
}

func (c *Controller) reconcileSandbox(ctx context.Context, req ctlruntime.Request, vm *v1alpha1.ComputeVM) (ctlruntime.Result, error) {
	if !vm.DeletionTimestamp.IsZero() {
		return c.release(ctx, req, vm)
	}
	if computevm.ReleaseRequested(vm) {
		return c.deleteOnRequest(ctx, req, vm)
	}
	st := vm.Status
	st.Conditions = append([]metav1.Condition(nil), st.Conditions...)
	st.Phase, st.Message = v1alpha1.ComputeVMPhaseRunning, "the deno sandbox runs on the bidder"
	setCondition(&st, vm.Generation, v1alpha1.ComputeVMConditionBackendBound, true, "DenoSandbox", "the bidder runs the sandbox itself")
	setCondition(&st, vm.Generation, v1alpha1.ComputeVMConditionReady, true, "Ready", st.Message)
	return ctlruntime.Result{}, c.write(ctx, req, vm, st)
}

func backendSupported(vm *v1alpha1.ComputeVM) bool {
	ref := vm.Spec.BackendRef
	return ref.Kind == v1alpha1.BackendKindFirecrackerVM && (ref.Group == "" || ref.Group == v1alpha1.GroupName) && ref.Name != ""
}

func (c *Controller) bind(ctx context.Context, req ctlruntime.Request, vm *v1alpha1.ComputeVM, have *v1alpha1.FirecrackerVM) (*v1alpha1.FirecrackerVM, error) {
	spec := firecracker.SpecFor(c.handle, vm.Spec)
	if have != nil && reflect.DeepEqual(have.Spec, spec) && hasOwner(have, vm) {
		return have, nil
	}
	if have != nil && !have.DeletionTimestamp.IsZero() {
		return have, nil
	}
	ctrl := true
	fvm := &v1alpha1.FirecrackerVM{
		TypeMeta: metav1.TypeMeta{APIVersion: v1alpha1.GroupVersion.String(), Kind: v1alpha1.BackendKindFirecrackerVM},
		ObjectMeta: metav1.ObjectMeta{Name: vm.Spec.BackendRef.Name, Labels: commonv1.ContractLabels(commonv1.ContractComputeVMs),
			OwnerReferences: []metav1.OwnerReference{{APIVersion: v1alpha1.GroupVersion.String(), Kind: "ComputeVM", Name: vm.Name, UID: vm.UID,
				Controller: &ctrl, BlockOwnerDeletion: &ctrl}}},
		Spec: spec,
	}
	if err := req.Apply(ctx, fvm); err != nil {
		return nil, err
	}
	if have == nil {
		c.log.Info("the FirecrackerVM backend is created", "computevm", vm.Name, "firecrackervm", fvm.Name)
	}
	return c.backend(ctx, req, vm)
}

func hasOwner(fvm *v1alpha1.FirecrackerVM, vm *v1alpha1.ComputeVM) bool {
	for _, o := range fvm.OwnerReferences {
		if o.UID == vm.UID && o.Controller != nil && *o.Controller {
			return true
		}
	}
	return false
}

func mirror(st v1alpha1.ComputeVMStatus, gen int64, fvm *v1alpha1.FirecrackerVM) v1alpha1.ComputeVMStatus {
	st.Conditions = append([]metav1.Condition(nil), st.Conditions...)
	st.ObservedGeneration = gen
	if fvm == nil {
		st.Phase, st.LostReason, st.Addresses, st.BootedAt = v1alpha1.ComputeVMPhasePending, "", nil, ""
		setCondition(&st, gen, v1alpha1.ComputeVMConditionReady, false, "BackendPending", "the backend is not created yet")
		return st
	}
	b := fvm.Status
	st.Phase, st.LostReason, st.BootedAt, st.Message = b.Phase, b.LostReason, b.BootedAt, b.Message
	if st.Phase == "" {
		st.Phase = v1alpha1.ComputeVMPhasePending
	}
	st.Addresses = nil
	if n := b.Network; n != nil && (n.GuestIP != "" || n.HostAddress != "") {
		st.Addresses = &v1alpha1.ComputeVMAddresses{Guest: n.GuestIP, Host: n.HostAddress}
	}
	ready := false
	reason, msg := "BackendNotReady", "the FirecrackerVM "+fvm.Name+" is not Ready"
	if c := apimeta.FindStatusCondition(b.Conditions, v1alpha1.FirecrackerVMConditionReady); c != nil && b.ObservedGeneration == fvm.Generation {
		ready = c.Status == metav1.ConditionTrue
		reason, msg = c.Reason, c.Message
	}
	setCondition(&st, gen, v1alpha1.ComputeVMConditionReady, ready, reason, msg)
	return st
}

func (c *Controller) write(ctx context.Context, req ctlruntime.Request, vm *v1alpha1.ComputeVM, st v1alpha1.ComputeVMStatus) error {
	st.Message = computevm.CapMessage(st.Message)
	prev := vm.Status.Phase
	if reflect.DeepEqual(vm.Status, st) {
		return nil
	}
	next := vm.DeepCopy()
	next.Status = st
	next.Status.Market = nil
	if err := req.ApplyStatus(ctx, next); err != nil {
		return err
	}
	if prev != st.Phase {
		c.log.Info("phase", "computevm", vm.Name, "from", string(prev), "to", string(st.Phase), "lostReason", string(st.LostReason),
			"message", st.Message)
	}
	return nil
}

func now() string { return time.Now().UTC().Format(time.RFC3339) }

func (c *Controller) declare(ctx context.Context, req ctlruntime.Request, drop string) {
	var list v1alpha1.ComputeVMList
	if err := req.List(ctx, &list); err != nil {
		c.log.Warn("listing the ComputeVMs to declare their guest material", "err", err.Error())
		return
	}
	names := make([]string, 0, len(list.Items))
	for _, vm := range list.Items {
		if isSandbox(&vm) {
			continue
		}
		if vm.Spec.Owner.Handle == c.handle && vm.Name != drop {
			names = append(names, vm.Name)
		}
	}
	if err := c.host.DeclareGuests(ctx, names); err != nil {
		c.log.Warn("declaring the live guests' material in the bidder's vault", "err", err.Error())
	}
}

func (c *Controller) Reconcile(ctx context.Context, req ctlruntime.Request, vm *v1alpha1.ComputeVM) (ctlruntime.Result, error) {
	c.declare(ctx, req, "")
	if !vm.DeletionTimestamp.IsZero() {
		return c.release(ctx, req, vm)
	}
	st := vm.Status
	st.Conditions = append([]metav1.Condition(nil), st.Conditions...)
	if isSandbox(vm) {
		return c.reconcileSandbox(ctx, req, vm)
	}
	if !backendSupported(vm) {
		setCondition(&st, vm.Generation, v1alpha1.ComputeVMConditionBackendBound, false, "UnsupportedBackend",
			fmt.Sprintf("spec.backendRef names %s/%s; this host runs FirecrackerVM", vm.Spec.BackendRef.Kind, vm.Spec.BackendRef.Name))
		setCondition(&st, vm.Generation, v1alpha1.ComputeVMConditionReady, false, "UnsupportedBackend", "no backend is bound")
		return ctlruntime.Result{}, c.write(ctx, req, vm, st)
	}
	if computevm.ReleaseRequested(vm) {
		return c.deleteOnRequest(ctx, req, vm)
	}
	have, err := c.backend(ctx, req, vm)
	if err != nil {
		return ctlruntime.Result{}, err
	}
	if have == nil {
		if err := c.host.EnsureGuestLogin(ctx, *vm); err != nil {
			if computevm.RefusedLogin(err) {
				st.Phase, st.Message = v1alpha1.ComputeVMPhaseFailed, err.Error()
				setCondition(&st, vm.Generation, v1alpha1.ComputeVMConditionReady, false, "GuestLoginRefused", err.Error())
				return ctlruntime.Result{}, c.write(ctx, req, vm, st)
			}
			st.Phase, st.Message = v1alpha1.ComputeVMPhasePending, "waiting for the guest login role: "+err.Error()
			if werr := c.write(ctx, req, vm, st); werr != nil {
				return ctlruntime.Result{}, werr
			}
			return ctlruntime.Result{}, err
		}
	}
	fvm, err := c.bind(ctx, req, vm, have)
	if err != nil {
		setCondition(&st, vm.Generation, v1alpha1.ComputeVMConditionBackendBound, false, "BindFailed", err.Error())
		if werr := c.write(ctx, req, vm, st); werr != nil {
			return ctlruntime.Result{}, werr
		}
		return ctlruntime.Result{}, err
	}
	st = mirror(st, vm.Generation, fvm)
	if fvm != nil {
		setCondition(&st, vm.Generation, v1alpha1.ComputeVMConditionBackendBound, true, "Bound", "FirecrackerVM "+fvm.Name)
	} else {
		setCondition(&st, vm.Generation, v1alpha1.ComputeVMConditionBackendBound, false, "BackendMissing", "FirecrackerVM "+vm.Spec.BackendRef.Name+" does not exist")
	}
	if computevm.Ended(&v1alpha1.ComputeVM{Status: st}) {
		if err := c.host.EnsureTerminationSync(ctx, *vm, computevm.GoneReason(st.LostReason), now(), nil); err != nil {
			c.log.Warn("creating the termination StateSync of an ended ComputeVM failed", "computevm", vm.Name, "err", err.Error())
		}
	}
	return ctlruntime.Result{}, c.write(ctx, req, vm, st)
}

func (c *Controller) deleteOnRequest(ctx context.Context, req ctlruntime.Request, vm *v1alpha1.ComputeVM) (ctlruntime.Result, error) {
	observedAt := computevm.ReleaseRequestedAt(vm)
	if observedAt == "" {
		observedAt = now()
	}
	if err := c.host.EnsureTerminationSync(ctx, *vm, "deleted_on_request", observedAt, computevm.ReleaseRequestRef(vm)); err != nil {
		return ctlruntime.Result{}, err
	}
	c.log.Info("the creator asks for the release; the ComputeVM is deleted and its finalizer releases the backend", "computevm", vm.Name, "backend", vm.Spec.BackendRef.Kind)
	return ctlruntime.Result{}, req.Delete(ctx, vm)
}

func (c *Controller) release(ctx context.Context, req ctlruntime.Request, vm *v1alpha1.ComputeVM) (ctlruntime.Result, error) {
	observedAt := computevm.ReleaseRequestedAt(vm)
	if observedAt == "" {
		observedAt = vm.DeletionTimestamp.UTC().Format(time.RFC3339)
	}
	if err := c.host.EnsureTerminationSync(ctx, *vm, "deleted_on_request", observedAt, computevm.ReleaseRequestRef(vm)); err != nil {
		return ctlruntime.Result{}, err
	}
	fvm, err := c.backend(ctx, req, vm)
	if err != nil {
		return ctlruntime.Result{}, err
	}
	st := mirror(vm.Status, vm.Generation, fvm)
	if fvm != nil {
		if st.Phase != v1alpha1.ComputeVMPhaseReleasing {
			st.Phase, st.Message = v1alpha1.ComputeVMPhaseReleasing, "the backend "+fvm.Name+" is released"
		}
		if err := c.write(ctx, req, vm, st); err != nil {
			return ctlruntime.Result{}, err
		}
		if fvm.DeletionTimestamp.IsZero() {
			if err := req.Delete(ctx, fvm); err != nil {
				return ctlruntime.Result{}, err
			}
			c.log.Info("the FirecrackerVM backend is deleted; its controller stops the VM", "computevm", vm.Name, "firecrackervm", fvm.Name)
		}
		return ctlruntime.Result{Waiting: true}, nil
	}
	c.host.DropGuestLogin(ctx, *vm)
	c.host.DropGuestGrants(ctx, vm.Name)
	c.declare(ctx, req, vm.Name)
	c.log.Info("released: the backend is gone and the finalizer is dropped", "computevm", vm.Name)
	return ctlruntime.Result{}, nil
}