Просмотр исходного кода

feat(tool): implement ManagedTool controller

Thomas Zhang 2 месяцев назад
Родитель
Сommit
85a9bb11f0
1 измененных файлов с 142 добавлено и 11 удалено
  1. 142 11
      internal/controller/managedtool_controller.go

+ 142 - 11
internal/controller/managedtool_controller.go

@@ -18,13 +18,22 @@ package controller
 
 import (
 	"context"
+	"fmt"
+	"maps"
 
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	apimeta "k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 
 	"github.com/LocoStack/loco-operator/api/v1alpha1"
+	"github.com/LocoStack/loco-operator/internal/reconciler"
+	"github.com/LocoStack/loco-operator/pkg/templates"
 )
 
 // ManagedToolReconciler reconciles a ManagedTool object
@@ -36,28 +45,150 @@ type ManagedToolReconciler struct {
 // +kubebuilder:rbac:groups=locostack.com,resources=managedtools,verbs=get;list;watch;create;update;patch;delete
 // +kubebuilder:rbac:groups=locostack.com,resources=managedtools/status,verbs=get;update;patch
 // +kubebuilder:rbac:groups=locostack.com,resources=managedtools/finalizers,verbs=update
+// +kubebuilder:rbac:groups=locostack.com,resources=components,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=locostack.com,resources=stacks,verbs=get;list;watch
 
-// Reconcile is part of the main kubernetes reconciliation loop which aims to
-// move the current state of the cluster closer to the desired state.
-// TODO(user): Modify the Reconcile function to compare the state specified by
-// the ManagedTool object against the actual cluster state, and then
-// perform operations to make the cluster state reflect the state specified by
-// the user.
-//
-// For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
 func (r *ManagedToolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	_ = logf.FromContext(ctx)
+	log := logf.FromContext(ctx)
 
-	// TODO(user): your logic here
+	mt := &v1alpha1.ManagedTool{}
+	if err := r.Get(ctx, req.NamespacedName, mt); err != nil {
+		if apierrors.IsNotFound(err) {
+			return ctrl.Result{}, nil
+		}
+		log.Error(err, "Failed to get ManagedTool", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
+		return ctrl.Result{}, err
+	}
 
+	patch := client.MergeFrom(mt.DeepCopy())
+
+	componentAvailable, err := r.reconcileComponent(ctx, mt)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+	gatewayBound, err := r.reconcileGatewayStatus(ctx, mt)
+	if err != nil {
+		log.Error(err, "Failed to reconcile gateway status for ManagedTool", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
+		return ctrl.Result{}, err
+	}
+	if !componentAvailable {
+		apimeta.SetStatusCondition(&mt.Status.Conditions, metav1.Condition{
+			Type:               "Available",
+			Status:             metav1.ConditionFalse,
+			Reason:             "ToolUnavailable",
+			Message:            "ManagedTool is not available",
+			ObservedGeneration: mt.Generation,
+		})
+	} else if !gatewayBound {
+		apimeta.SetStatusCondition(&mt.Status.Conditions, metav1.Condition{
+			Type:               "Available",
+			Status:             metav1.ConditionFalse,
+			Reason:             "GatewayUnbound",
+			Message:            "ManagedTool is not bound to a gateway",
+			ObservedGeneration: mt.Generation,
+		})
+	} else {
+		apimeta.SetStatusCondition(&mt.Status.Conditions, metav1.Condition{
+			Type:               "Available",
+			Status:             metav1.ConditionTrue,
+			Reason:             "ToolAvailable",
+			Message:            "ManagedTool is available",
+			ObservedGeneration: mt.Generation,
+		})
+	}
+
+	mt.Status.ObservedGeneration = mt.Generation
+	if err := r.Status().Patch(ctx, mt, patch); err != nil {
+		return ctrl.Result{}, client.IgnoreNotFound(err)
+	}
+
+	log.Info("Reconciled ManagedTool", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
 	return ctrl.Result{}, nil
 }
 
+func (r *ManagedToolReconciler) reconcileComponent(ctx context.Context, mt *v1alpha1.ManagedTool) (bool, error) {
+	compName := fmt.Sprintf("tool-%s", mt.Name)
+	comp := &v1alpha1.Component{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      compName,
+			Namespace: mt.Namespace,
+		},
+	}
+	err := r.Get(ctx, client.ObjectKey{Namespace: mt.Namespace, Name: compName}, comp)
+
+	tmpl, err := templates.Manager.ResolveTemplate(mt.Spec.Template, "")
+	if err != nil {
+		apimeta.SetStatusCondition(&mt.Status.Conditions, metav1.Condition{
+			Type:               "ToolRuntimeResolved",
+			Status:             metav1.ConditionFalse,
+			Reason:             "TemplateNotResolved",
+			Message:            fmt.Sprintf("Failed to reconcile template for ManagedTool: %v", err),
+			ObservedGeneration: mt.Generation,
+		})
+		return false, nil
+	}
+	apimeta.SetStatusCondition(&mt.Status.Conditions, metav1.Condition{
+		Type:               "ToolRuntimeResolved",
+		Status:             metav1.ConditionTrue,
+		Reason:             "TemplateResolved",
+		Message:            fmt.Sprintf("Successfully resolved template for ManagedTool: %v", tmpl.Name),
+		ObservedGeneration: mt.Generation,
+	})
+
+	_, err = controllerutil.CreateOrUpdate(ctx, r.Client, comp, func() error {
+		if comp.Labels == nil {
+			comp.Labels = make(map[string]string)
+		}
+		stackName := ""
+		if mt.Spec.StackRef != nil {
+			stackName = mt.Spec.StackRef.Name
+		}
+		maps.Copy(comp.Labels, reconciler.ResourceLabels(stackName, "Tool", mt.Name, ""))
+		comp.Spec.Category = "Tool"
+		comp.Spec.Template = &tmpl
+		if mt.Spec.StackRef != nil {
+			comp.Spec.StackRef = &corev1.LocalObjectReference{Name: mt.Spec.StackRef.Name}
+		}
+		return controllerutil.SetControllerReference(mt, comp, r.Scheme)
+	})
+
+	available := apimeta.IsStatusConditionTrue(comp.Status.Conditions, "Available")
+	if available {
+		mt.Status.Endpoint = comp.Status.Endpoint
+		apimeta.SetStatusCondition(&mt.Status.Conditions, metav1.Condition{
+			Type:               "Ready",
+			Status:             metav1.ConditionTrue,
+			Reason:             "ComponentAvailable",
+			Message:            "Component is available",
+			ObservedGeneration: mt.Generation,
+		})
+	} else {
+		apimeta.SetStatusCondition(&mt.Status.Conditions, metav1.Condition{
+			Type:               "Ready",
+			Status:             metav1.ConditionFalse,
+			Reason:             "ComponentUnavailable",
+			Message:            "Component is not available",
+			ObservedGeneration: mt.Generation,
+		})
+	}
+	return available, err
+}
+
+func (r *ManagedToolReconciler) reconcileGatewayStatus(ctx context.Context, mt *v1alpha1.ManagedTool) (bool, error) {
+	condition, err := reconciler.ReconcileGatewayStatus(ctx, r.Client, r.Scheme, mt.Spec.StackRef.Name, mt)
+	if err != nil {
+		return false, err
+	}
+	apimeta.SetStatusCondition(&mt.Status.Conditions, *condition)
+	return condition.Status == metav1.ConditionTrue, nil
+}
+
 // SetupWithManager sets up the controller with the Manager.
 func (r *ManagedToolReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&v1alpha1.ManagedTool{}).
+		Owns(&v1alpha1.Component{}).
+		Watches(&v1alpha1.Component{}, ComponentEventHandler("ManagedTool")).
 		Named("managedtool").
 		Complete(r)
 }