component_controller.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  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. appsv1 "k8s.io/api/apps/v1"
  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. ctrl "sigs.k8s.io/controller-runtime"
  24. "sigs.k8s.io/controller-runtime/pkg/client"
  25. "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
  26. logf "sigs.k8s.io/controller-runtime/pkg/log"
  27. "github.com/LocoStack/loco-operator/api/v1alpha1"
  28. "github.com/LocoStack/loco-operator/internal/reconciler"
  29. "github.com/LocoStack/loco-operator/pkg/templates"
  30. )
  31. // ComponentReconciler reconciles a Component object
  32. type ComponentReconciler struct {
  33. client.Client
  34. Scheme *runtime.Scheme
  35. }
  36. // +kubebuilder:rbac:groups=locostack.com,resources=components,verbs=get;list;watch;create;update;patch;delete
  37. // +kubebuilder:rbac:groups=locostack.com,resources=components/status,verbs=get;update;patch
  38. // +kubebuilder:rbac:groups=locostack.com,resources=components/finalizers,verbs=update
  39. // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch
  40. // +kubebuilder:rbac:groups="",resources=persistentvolumes,verbs=get;list;watch;create;update;patch
  41. // +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch
  42. // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch
  43. // +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch
  44. // +kubebuilder:rbac:groups="apps",resources=deployments,verbs=get;list;watch;create;update;patch
  45. func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  46. log := logf.FromContext(ctx)
  47. comp := &v1alpha1.Component{}
  48. if err := r.Get(ctx, req.NamespacedName, comp); err != nil {
  49. if apierrors.IsNotFound(err) {
  50. return ctrl.Result{}, nil
  51. }
  52. log.Error(err, "Failed to get Component", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
  53. return ctrl.Result{}, err
  54. }
  55. patch := client.MergeFrom(comp.DeepCopy())
  56. tmpl, err := templates.Manager.ResolveTemplate(comp.Spec.Template, "")
  57. if err != nil {
  58. apimeta.SetStatusCondition(&comp.Status.Conditions, metav1.Condition{
  59. Type: "Reconciled",
  60. Status: metav1.ConditionFalse,
  61. Reason: "TemplateResolutionFailed",
  62. Message: fmt.Sprintf("Could not resolve template: %v", err),
  63. ObservedGeneration: comp.Generation,
  64. })
  65. log.Error(err, "Failed to resolve template for Component", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
  66. _ = r.Status().Patch(ctx, comp, patch)
  67. return ctrl.Result{}, nil
  68. }
  69. stack := &v1alpha1.Stack{}
  70. if comp.Spec.StackRef == nil || comp.Spec.StackRef.Name == "" {
  71. stack = nil
  72. } else if err := r.Get(ctx, client.ObjectKey{Namespace: comp.Namespace, Name: comp.Spec.StackRef.Name}, stack); err != nil {
  73. stack = nil
  74. }
  75. componentReconciler := r.getComponentReconciler(comp, stack)
  76. dependencies, err := componentReconciler.ReconcileComponent(ctx, &tmpl, make(map[string]string))
  77. if err != nil {
  78. apimeta.SetStatusCondition(&comp.Status.Conditions, metav1.Condition{
  79. Type: "Reconciled",
  80. Status: metav1.ConditionFalse,
  81. Reason: "ReconcileFailed",
  82. Message: fmt.Sprintf("Could not reconcile component: %v", err),
  83. ObservedGeneration: comp.Generation,
  84. })
  85. log.Error(err, "Failed to reconcile Component", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
  86. _ = r.Status().Patch(ctx, comp, patch)
  87. return ctrl.Result{}, nil
  88. }
  89. comp.Status.Endpoint = fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", componentReconciler.ResourceName(""), comp.Namespace, tmpl.Spec.Runtime.Port)
  90. comp.Status.ObservedGeneration = comp.Generation
  91. if len(dependencies) > 0 {
  92. comp.Status.Dependencies = make(map[string]map[string]int64)
  93. for _, dep := range dependencies {
  94. gvk, err := apiutil.GVKForObject(dep, r.Scheme)
  95. if err != nil {
  96. log.Error(err, "Failed to get GVK for dependency", "dependency", dep)
  97. continue
  98. }
  99. if gvk.Group != v1alpha1.GroupVersion.Group {
  100. continue
  101. }
  102. kind := gvk.Kind
  103. name := dep.GetName()
  104. generation := dep.GetGeneration()
  105. if _, exists := comp.Status.Dependencies[kind]; !exists {
  106. comp.Status.Dependencies[kind] = make(map[string]int64)
  107. }
  108. comp.Status.Dependencies[kind][name] = generation
  109. }
  110. }
  111. apimeta.SetStatusCondition(&comp.Status.Conditions, metav1.Condition{
  112. Type: "Reconciled",
  113. Status: metav1.ConditionTrue,
  114. Reason: "ReconcileSucceeded",
  115. Message: "Component reconciled",
  116. ObservedGeneration: comp.Generation,
  117. })
  118. if available := componentReconciler.ComponentAvailable(ctx); !available {
  119. apimeta.SetStatusCondition(&comp.Status.Conditions, metav1.Condition{
  120. Type: "Available",
  121. Status: metav1.ConditionFalse,
  122. Reason: "ServiceUnavailable",
  123. Message: "Component is not available",
  124. ObservedGeneration: comp.Generation,
  125. })
  126. } else {
  127. apimeta.SetStatusCondition(&comp.Status.Conditions, metav1.Condition{
  128. Type: "Available",
  129. Status: metav1.ConditionTrue,
  130. Reason: "ComponentReady",
  131. Message: "Component is available",
  132. ObservedGeneration: comp.Generation,
  133. })
  134. }
  135. if err := r.Status().Patch(ctx, comp, patch); err != nil {
  136. return ctrl.Result{}, client.IgnoreNotFound(err)
  137. }
  138. log.Info("Reconciled Component", "namespace", comp.Namespace, "name", comp.Name)
  139. return ctrl.Result{}, nil
  140. }
  141. func (r *ComponentReconciler) getComponentReconciler(comp *v1alpha1.Component, stack *v1alpha1.Stack) reconciler.ComponentReconciler {
  142. switch comp.Spec.Template.Name {
  143. case "litellm":
  144. return reconciler.NewLiteLLMReconciler(r.Client, r.Scheme, stack, comp)
  145. case "qdrant":
  146. return reconciler.NewQdrantReconciler(r.Client, r.Scheme, stack, comp)
  147. case "neo4j":
  148. return reconciler.NewNeo4jReconciler(r.Client, r.Scheme, stack, comp)
  149. case "postgresql":
  150. return reconciler.NewPostgreSQLReconciler(r.Client, r.Scheme, stack, comp)
  151. case "phoenix":
  152. return reconciler.NewPhoenixReconciler(r.Client, r.Scheme, stack, comp)
  153. default:
  154. return reconciler.NewDefaultComponentReconciler(r.Client, r.Scheme, stack, comp)
  155. }
  156. }
  157. // SetupWithManager sets up the controller with the Manager.
  158. func (r *ComponentReconciler) SetupWithManager(mgr ctrl.Manager) error {
  159. return ctrl.NewControllerManagedBy(mgr).
  160. For(&v1alpha1.Component{}).
  161. Owns(&corev1.ConfigMap{}).
  162. Owns(&corev1.Secret{}).
  163. Owns(&corev1.PersistentVolumeClaim{}).
  164. Owns(&corev1.Service{}).
  165. Owns(&appsv1.Deployment{}).
  166. Named("component").
  167. Complete(r)
  168. }