agent_controller.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. /*
  2. Copyright 2026 LocoStack.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package controller
  14. import (
  15. "context"
  16. "fmt"
  17. "maps"
  18. corev1 "k8s.io/api/core/v1"
  19. apierrors "k8s.io/apimachinery/pkg/api/errors"
  20. apimeta "k8s.io/apimachinery/pkg/api/meta"
  21. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  22. "k8s.io/apimachinery/pkg/runtime"
  23. "k8s.io/apimachinery/pkg/types"
  24. ctrl "sigs.k8s.io/controller-runtime"
  25. "sigs.k8s.io/controller-runtime/pkg/client"
  26. "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
  27. "sigs.k8s.io/controller-runtime/pkg/handler"
  28. logf "sigs.k8s.io/controller-runtime/pkg/log"
  29. "sigs.k8s.io/controller-runtime/pkg/reconcile"
  30. "github.com/LocoStack/loco-operator/api/v1alpha1"
  31. "github.com/LocoStack/loco-operator/internal/reconciler"
  32. "github.com/LocoStack/loco-operator/pkg/templates"
  33. )
  34. // AgentReconciler reconciles a Agent object
  35. type AgentReconciler struct {
  36. client.Client
  37. Scheme *runtime.Scheme
  38. }
  39. // +kubebuilder:rbac:groups=locostack.com,resources=agents,verbs=get;list;watch;create;update;patch;delete
  40. // +kubebuilder:rbac:groups=locostack.com,resources=agents/status,verbs=get;update;patch
  41. // +kubebuilder:rbac:groups=locostack.com,resources=agents/finalizers,verbs=update
  42. // +kubebuilder:rbac:groups=locostack.com,resources=components,verbs=get;list;watch;create;update;patch;delete
  43. // +kubebuilder:rbac:groups=locostack.com,resources=stacks,verbs=get;list;watch
  44. func (r *AgentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  45. log := logf.FromContext(ctx)
  46. agent := &v1alpha1.Agent{}
  47. if err := r.Get(ctx, req.NamespacedName, agent); err != nil {
  48. if apierrors.IsNotFound(err) {
  49. return ctrl.Result{}, nil
  50. }
  51. log.Error(err, "Failed to get Agent", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
  52. return ctrl.Result{}, err
  53. }
  54. patch := client.MergeFrom(agent.DeepCopy())
  55. componentAvailable, err := r.reconcileComponent(ctx, agent)
  56. if err != nil {
  57. return ctrl.Result{}, err
  58. }
  59. if !componentAvailable {
  60. apimeta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{
  61. Type: "Available",
  62. Status: metav1.ConditionFalse,
  63. Reason: "AgentUnavailable",
  64. Message: "Agent is not available",
  65. ObservedGeneration: agent.Generation,
  66. })
  67. } else {
  68. apimeta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{
  69. Type: "Available",
  70. Status: metav1.ConditionTrue,
  71. Reason: "AgentAvailable",
  72. Message: "Agent is available",
  73. ObservedGeneration: agent.Generation,
  74. })
  75. }
  76. agent.Status.ObservedGeneration = agent.Generation
  77. if err := r.Status().Patch(ctx, agent, patch); err != nil {
  78. return ctrl.Result{}, client.IgnoreNotFound(err)
  79. }
  80. log.Info("Reconciled Agent", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
  81. return ctrl.Result{}, nil
  82. }
  83. func (r *AgentReconciler) reconcileComponent(ctx context.Context, agent *v1alpha1.Agent) (bool, error) {
  84. compName := fmt.Sprintf("agent-%s", agent.Name)
  85. comp := &v1alpha1.Component{
  86. ObjectMeta: metav1.ObjectMeta{
  87. Name: compName,
  88. Namespace: agent.Namespace,
  89. },
  90. }
  91. err := r.Get(ctx, client.ObjectKey{Namespace: agent.Namespace, Name: compName}, comp)
  92. tmpl, err := templates.Manager.ResolveTemplate(agent.Spec.Template, "")
  93. if err != nil {
  94. apimeta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{
  95. Type: "AgentRuntimeResolved",
  96. Status: metav1.ConditionFalse,
  97. Reason: "TemplateNotResolved",
  98. Message: fmt.Sprintf("Failed to reconcile template for Agent: %v", err),
  99. ObservedGeneration: agent.Generation,
  100. })
  101. return false, nil
  102. }
  103. apimeta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{
  104. Type: "AgentRuntimeResolved",
  105. Status: metav1.ConditionTrue,
  106. Reason: "TemplateResolved",
  107. Message: fmt.Sprintf("Successfully resolved template for Agent: %v", tmpl.Name),
  108. ObservedGeneration: agent.Generation,
  109. })
  110. _, err = controllerutil.CreateOrUpdate(ctx, r.Client, comp, func() error {
  111. if comp.Labels == nil {
  112. comp.Labels = make(map[string]string)
  113. }
  114. stackName := ""
  115. if agent.Spec.StackRef != nil {
  116. stackName = agent.Spec.StackRef.Name
  117. }
  118. maps.Copy(comp.Labels, reconciler.ResourceLabels(stackName, "Agent", agent.Name, ""))
  119. dependencies := make([]corev1.ObjectReference, 0)
  120. dependencies = append(dependencies, agent.Spec.Model)
  121. for _, tool := range agent.Spec.Tools {
  122. dependencies = append(dependencies, tool)
  123. }
  124. for _, kb := range agent.Spec.KnowledgeBases {
  125. dependencies = append(dependencies, kb)
  126. }
  127. comp.Spec.Category = "Agent"
  128. comp.Spec.Template = &tmpl
  129. comp.Spec.Variables = map[string]string{
  130. "systemPrompt": agent.Spec.SystemPrompt,
  131. }
  132. comp.Spec.Dependencies = dependencies
  133. if agent.Spec.StackRef != nil {
  134. comp.Spec.StackRef = &corev1.LocalObjectReference{Name: agent.Spec.StackRef.Name}
  135. }
  136. return controllerutil.SetControllerReference(agent, comp, r.Scheme)
  137. })
  138. available := apimeta.IsStatusConditionTrue(comp.Status.Conditions, "Available")
  139. if available {
  140. agent.Status.Endpoint = comp.Status.Endpoint
  141. apimeta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{
  142. Type: "Ready",
  143. Status: metav1.ConditionTrue,
  144. Reason: "ComponentAvailable",
  145. Message: "Component is available",
  146. ObservedGeneration: agent.Generation,
  147. })
  148. } else {
  149. apimeta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{
  150. Type: "Ready",
  151. Status: metav1.ConditionFalse,
  152. Reason: "ComponentUnavailable",
  153. Message: "Component is not available",
  154. ObservedGeneration: agent.Generation,
  155. })
  156. }
  157. return available, err
  158. }
  159. // SetupWithManager sets up the controller with the Manager.
  160. func (r *AgentReconciler) SetupWithManager(mgr ctrl.Manager) error {
  161. mapComponentToAgents := func(ctx context.Context, obj client.Object) []reconcile.Request {
  162. comp, ok := obj.(*v1alpha1.Component)
  163. if !ok {
  164. return nil
  165. }
  166. var reqs []reconcile.Request
  167. for _, em := range comp.Spec.Dependencies {
  168. if em.APIVersion == v1alpha1.GroupVersion.Group && em.Kind == "Agent" {
  169. reqs = append(reqs, reconcile.Request{
  170. NamespacedName: types.NamespacedName{
  171. Name: em.Name,
  172. Namespace: em.Namespace,
  173. },
  174. })
  175. }
  176. }
  177. return reqs
  178. }
  179. return ctrl.NewControllerManagedBy(mgr).
  180. For(&v1alpha1.Agent{}).
  181. Owns(&v1alpha1.Component{}).
  182. Watches(&v1alpha1.Component{}, handler.EnqueueRequestsFromMapFunc(mapComponentToAgents)).
  183. Named("agent").
  184. Complete(r)
  185. }