Explorar o código

feat(tool): implement ExternalTool controller

Thomas Zhang hai 2 meses
pai
achega
f73e02db71

+ 0 - 1
api/v1alpha1/externaltool_types.go

@@ -28,7 +28,6 @@ type ExternalToolSpec struct {
 	StackRef *corev1.LocalObjectReference `json:"stackRef,omitempty"`
 
 	// toolName is the logical name for this tool as seen by the gateway and agents.
-	// Used as the mcp_servers key in the LiteLLM config.
 	// +kubebuilder:validation:Required
 	ToolName string `json:"toolName"`
 

+ 2 - 3
config/crd/bases/locostack.com_externaltools.yaml

@@ -283,9 +283,8 @@ spec:
                 type: object
                 x-kubernetes-map-type: atomic
               toolName:
-                description: |-
-                  toolName is the logical name for this tool as seen by the gateway and agents.
-                  Used as the mcp_servers key in the LiteLLM config.
+                description: toolName is the logical name for this tool as seen by
+                  the gateway and agents.
                 type: string
               transport:
                 default: http

+ 71 - 2
internal/controller/externaltool_controller.go

@@ -19,12 +19,16 @@ package controller
 import (
 	"context"
 
+	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"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 
 	"github.com/LocoStack/loco-operator/api/v1alpha1"
+	"github.com/LocoStack/loco-operator/internal/reconciler"
 )
 
 // ExternalToolReconciler reconciles a ExternalTool object
@@ -36,19 +40,84 @@ type ExternalToolReconciler struct {
 // +kubebuilder:rbac:groups=locostack.com,resources=externaltools,verbs=get;list;watch;create;update;patch;delete
 // +kubebuilder:rbac:groups=locostack.com,resources=externaltools/status,verbs=get;update;patch
 // +kubebuilder:rbac:groups=locostack.com,resources=externaltools/finalizers,verbs=update
+// +kubebuilder:rbac:groups=locostack.com,resources=components,verbs=get;list;watch
+// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
 
 func (r *ExternalToolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	_ = logf.FromContext(ctx)
+	log := logf.FromContext(ctx)
 
-	// TODO(user): your logic here
+	et := &v1alpha1.ExternalTool{}
+	if err := r.Get(ctx, req.NamespacedName, et); err != nil {
+		if apierrors.IsNotFound(err) {
+			return ctrl.Result{}, nil
+		}
+		log.Error(err, "Failed to get ExternalTool", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
+		return ctrl.Result{}, err
+	}
 
+	patch := client.MergeFrom(et.DeepCopy())
+
+	gatewayBound, err := r.reconcileGatewayStatus(ctx, et)
+	if err != nil {
+		log.Error(err, "Failed to reconcile gateway status for ExternalTool", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
+		return ctrl.Result{}, err
+	}
+	credentialsResolved, err := r.reconcileCredentials(ctx, et)
+	if err != nil {
+		log.Error(err, "Failed to reconcile credentials for ExternalTool", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
+		return ctrl.Result{}, err
+	}
+
+	if gatewayBound && credentialsResolved {
+		apimeta.SetStatusCondition(&et.Status.Conditions, metav1.Condition{
+			Type:               "Ready",
+			Status:             metav1.ConditionTrue,
+			Reason:             "AllPreconditionsMet",
+			Message:            "Gateway exists and credentials resolve",
+			ObservedGeneration: et.Generation,
+		})
+	} else {
+		apimeta.SetStatusCondition(&et.Status.Conditions, metav1.Condition{
+			Type:               "Ready",
+			Status:             metav1.ConditionFalse,
+			Reason:             "PreconditionsNotMet",
+			Message:            "Gateway not found or credentials not resolved",
+			ObservedGeneration: et.Generation,
+		})
+	}
+
+	et.Status.ObservedGeneration = et.Generation
+	if err := r.Status().Patch(ctx, et, patch); err != nil {
+		return ctrl.Result{}, client.IgnoreNotFound(err)
+	}
+
+	log.Info("Reconciled ExternalTool", "namespace", et.Namespace, "name", et.Name)
 	return ctrl.Result{}, nil
 }
 
+func (r *ExternalToolReconciler) reconcileGatewayStatus(ctx context.Context, et *v1alpha1.ExternalTool) (bool, error) {
+	condition, err := reconciler.ReconcileGatewayStatus(ctx, r.Client, r.Scheme, et.Spec.StackRef.Name, et)
+	if err != nil {
+		return false, err
+	}
+	apimeta.SetStatusCondition(&et.Status.Conditions, *condition)
+	return condition.Status == metav1.ConditionTrue, nil
+}
+
+func (r *ExternalToolReconciler) reconcileCredentials(ctx context.Context, et *v1alpha1.ExternalTool) (bool, error) {
+	condition, err := reconciler.ReconcileCredentials(ctx, r.Client, r.Scheme, et.Spec.Auth, et)
+	if err != nil {
+		return false, err
+	}
+	apimeta.SetStatusCondition(&et.Status.Conditions, *condition)
+	return condition.Status == metav1.ConditionTrue, nil
+}
+
 // SetupWithManager sets up the controller with the Manager.
 func (r *ExternalToolReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&v1alpha1.ExternalTool{}).
+		Watches(&v1alpha1.Component{}, ComponentEventHandler("ExternalTool")).
 		Named("externaltool").
 		Complete(r)
 }