~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
168 lines · 5.4 KB Raw History
package componentcore

import (
	"context"
	"errors"
	"fmt"
	"time"

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

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

const RestartFieldManager = "socialweb.computer/dev-restart"

type DynamicFactory = kube.Factory

type componentKind struct {
	Kind string
	GVR  schema.GroupVersionResource
}

var runtimeKinds = []componentKind{
	{compose.ComponentKindBidder, compose.BidderGVR},
	{compose.ComponentKindGuestControlPlane, compose.GuestControlPlaneGVR},
	{compose.ComponentKindPds, compose.PdsGVR},
	{compose.ComponentKindRequester, compose.RequesterGVR},
	{compose.ComponentKindGatewayAPI, compose.GatewayAPIGVR},
	{compose.ComponentKindSshServer, compose.SshServerGVR},
	{compose.ComponentKindPlc, compose.PlcGVR},
	{compose.ComponentKindRelay, compose.RelayGVR},
	{compose.ComponentKindIngressProxy, compose.IngressProxyGVR},
	{compose.ComponentKindDispatcherTLS, compose.DispatcherTLSGVR},
}

type ComponentRef struct {
	Workspace string
	Kind      string
	Name      string
	GVR       schema.GroupVersionResource
}

type RestartStatus struct {
	ObservedGeneration int64               `json:"observedGeneration,omitempty"`
	Phase              string              `json:"phase,omitempty"`
	Ready              bool                `json:"ready"`
	Message            string              `json:"message,omitempty"`
	Restarts           int32               `json:"restarts,omitempty"`
	StartedAt          string              `json:"startedAt,omitempty"`
	Endpoints          []v1alpha1.Endpoint `json:"endpoints,omitempty"`
}

func FindComponents(ctx context.Context, dynamic DynamicFactory, workspaces []string, kind, name string) ([]ComponentRef, error) {
	var out []ComponentRef
	for _, workspace := range workspaces {
		dyn, err := dynamic(workspace)
		if err != nil {
			return nil, err
		}
		for _, candidate := range runtimeKinds {
			if kind != "" && candidate.Kind != kind {
				continue
			}
			u, err := kube.Get[metav1.PartialObjectMetadata](ctx, dyn.Resource(candidate.GVR), name)
			if apierrors.IsNotFound(err) || apierrors.IsForbidden(err) {
				continue
			}
			if err != nil {
				return nil, fmt.Errorf("componentcore: reading %s %s in %s: %w", candidate.Kind, name, workspace, err)
			}
			out = append(out, ComponentRef{Workspace: workspace, Kind: candidate.Kind, Name: u.GetName(), GVR: candidate.GVR})
		}
	}
	return out, nil
}

func DescribeComponents(ctx context.Context, dynamic DynamicFactory, workspaces []string) []string {
	var out []string
	for _, workspace := range workspaces {
		dyn, err := dynamic(workspace)
		if err != nil {
			continue
		}
		for _, kind := range runtimeKinds {
			list, err := kube.List[metav1.PartialObjectMetadata](ctx, dyn.Resource(kind.GVR), metav1.ListOptions{})
			if err != nil {
				continue
			}
			for i := range list {
				out = append(out, workspace+":"+kind.Kind+"/"+list[i].GetName())
			}
		}
	}
	return out
}

func ReadStatus(ctx context.Context, dynamic DynamicFactory, ref ComponentRef) (RestartStatus, error) {
	dyn, err := dynamic(ref.Workspace)
	if err != nil {
		return RestartStatus{}, err
	}
	u, err := kube.Get[componentStatus](ctx, dyn.Resource(ref.GVR), ref.Name)
	if err != nil {
		return RestartStatus{}, err
	}
	return u.Status, nil
}

func Restart(ctx context.Context, dynamic DynamicFactory, ref ComponentRef) (string, error) {
	dyn, err := dynamic(ref.Workspace)
	if err != nil {
		return "", err
	}
	stamp := time.Now().UTC().Format(time.RFC3339Nano)
	body := kube.Doc{"apiVersion": ref.GVR.GroupVersion().String(), "kind": ref.Kind,
		"metadata": map[string]any{"name": ref.Name}, "spec": map[string]any{"restartedAt": stamp}}
	if _, err := kube.Apply[kube.Doc](ctx, dyn.Resource(ref.GVR), body,
		kube.ApplyOptions{FieldManager: RestartFieldManager, Force: true}); err != nil {
		return "", fmt.Errorf("componentcore: requesting the restart of %s/%s: %w", ref.Kind, ref.Name, err)
	}
	return stamp, nil
}

func WaitRestarted(ctx context.Context, dynamic DynamicFactory, ref ComponentRef, stamp string, before RestartStatus, timeout time.Duration) (RestartStatus, error) {
	requested, err := time.Parse(time.RFC3339Nano, stamp)
	if err != nil {
		return RestartStatus{}, err
	}
	dyn, err := dynamic(ref.Workspace)
	if err != nil {
		return RestartStatus{}, err
	}
	last := before
	_, err = kube.Await(ctx, dyn.Resource(ref.GVR), ref.Name, timeout, func(u *componentStatus) (bool, error) {
		if u == nil {
			return false, nil
		}
		last = u.Status
		return last.Ready && restartedAfter(last, before, requested), nil
	})
	if errors.Is(err, kube.ErrAwaitTimeout) {
		return last, fmt.Errorf("componentcore: %s/%s in %s did not restart within %s (phase %s, ready %v, restarts %d, startedAt %s)",
			ref.Kind, ref.Name, ref.Workspace, timeout, last.Phase, last.Ready, last.Restarts, last.StartedAt)
	}
	return last, err
}

func restartedAfter(cur, before RestartStatus, requested time.Time) bool {
	if cur.StartedAt != "" {
		if started, err := time.Parse(time.RFC3339, cur.StartedAt); err == nil {
			if !started.Before(requested.Truncate(time.Second)) {
				return true
			}
		}
	}
	if cur.Restarts > before.Restarts {
		return true
	}
	return false
}

type componentStatus struct {
	Status RestartStatus `json:"status"`
}