helper.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  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. "github.com/LocoStack/loco-operator/api/v1alpha1"
  17. "github.com/LocoStack/loco-operator/internal/reconciler"
  18. corev1 "k8s.io/api/core/v1"
  19. "k8s.io/apimachinery/pkg/runtime/schema"
  20. "k8s.io/apimachinery/pkg/types"
  21. "sigs.k8s.io/controller-runtime/pkg/client"
  22. "sigs.k8s.io/controller-runtime/pkg/handler"
  23. logf "sigs.k8s.io/controller-runtime/pkg/log"
  24. "sigs.k8s.io/controller-runtime/pkg/reconcile"
  25. )
  26. func ComponentEventHandler(kind string) handler.EventHandler {
  27. mapFunc := func(ctx context.Context, obj client.Object) []reconcile.Request {
  28. comp, ok := obj.(*v1alpha1.Component)
  29. if !ok {
  30. return nil
  31. }
  32. var reqs []reconcile.Request
  33. for _, dep := range comp.Spec.Dependencies {
  34. if !dependencyRefMatches(dep, kind) {
  35. continue
  36. }
  37. namespace := dep.Namespace
  38. if namespace == "" {
  39. namespace = comp.GetNamespace()
  40. }
  41. reqs = append(reqs, reconcile.Request{
  42. NamespacedName: types.NamespacedName{
  43. Name: dep.Name,
  44. Namespace: namespace,
  45. },
  46. })
  47. }
  48. return reqs
  49. }
  50. return handler.EnqueueRequestsFromMapFunc(mapFunc)
  51. }
  52. func dependencyRefMatches(ref corev1.ObjectReference, kind string) bool {
  53. if ref.Kind != kind {
  54. return false
  55. }
  56. if ref.APIVersion == "" {
  57. return true
  58. }
  59. gv, err := schema.ParseGroupVersion(ref.APIVersion)
  60. return err == nil && gv.Group == v1alpha1.GroupVersion.Group
  61. }
  62. // stackComponentCategories are the Component categories that the rest of a Stack resolves
  63. // implicitly, through the Stack rather than through spec.dependencies: the gateway URL and
  64. // master key, the vector and graph store URLs and the tracing collector endpoint are all
  65. // rendered into the configs of the other Components, so a change to one of these has to
  66. // re-render the rest of the Stack.
  67. var stackComponentCategories = map[string]bool{
  68. "Gateway": true,
  69. "VectorStore": true,
  70. "GraphStore": true,
  71. "Observability": true,
  72. }
  73. // SkillConfigMapEventHandler maps a Skill's ConfigMap to the Components that mount it. The
  74. // ConfigMap is owned by the Skill rather than by the Component, so watching the Skill alone
  75. // would race its ConfigMap: a Component reconciled off the Skill event can still read the
  76. // previous file set, and no further event would arrive to correct it.
  77. func SkillConfigMapEventHandler(c client.Client) handler.EventHandler {
  78. return handler.EnqueueRequestsFromMapFunc(skillConfigMapMapFunc(c))
  79. }
  80. func skillConfigMapMapFunc(c client.Client) handler.MapFunc {
  81. return func(ctx context.Context, obj client.Object) []reconcile.Request {
  82. if _, ok := obj.GetAnnotations()[v1alpha1.SkillFilePathsAnnotation]; !ok {
  83. return nil
  84. }
  85. return dependentComponentRequests(ctx, c, obj.GetNamespace(), func(comp *v1alpha1.Component) bool {
  86. return componentDependsOn(comp, "Skill", obj.GetName(), obj.GetNamespace())
  87. })
  88. }
  89. }
  90. // AuthSecretEventHandler maps a Secret to the Components whose config embeds a credential
  91. // read from it. Model and tool credentials reach a Pod as env vars sourced from these
  92. // Secrets, and a Pod's environment is fixed for its lifetime, so a rotated key only takes
  93. // effect once the Pod is replaced - which needs the Components depending on that model or
  94. // tool to re-render and roll out.
  95. func AuthSecretEventHandler(c client.Client) handler.EventHandler {
  96. return handler.EnqueueRequestsFromMapFunc(authSecretMapFunc(c))
  97. }
  98. func authSecretMapFunc(c client.Client) handler.MapFunc {
  99. return func(ctx context.Context, obj client.Object) []reconcile.Request {
  100. log := logf.FromContext(ctx)
  101. namespace := obj.GetNamespace()
  102. // kind -> names of the dependencies reading a credential from this Secret
  103. refs := make(map[string]map[string]bool)
  104. addRef := func(kind, name string, auth *v1alpha1.AuthSpec) {
  105. if !authReadsSecret(auth, obj.GetName()) {
  106. return
  107. }
  108. if refs[kind] == nil {
  109. refs[kind] = make(map[string]bool)
  110. }
  111. refs[kind][name] = true
  112. }
  113. emList := &v1alpha1.ExternalModelList{}
  114. if err := c.List(ctx, emList, client.InNamespace(namespace)); err != nil {
  115. log.Error(err, "Failed to list ExternalModels for Secret event", "namespace", namespace, "secret", obj.GetName())
  116. return nil
  117. }
  118. for i := range emList.Items {
  119. addRef("ExternalModel", emList.Items[i].Name, emList.Items[i].Spec.Auth)
  120. }
  121. etList := &v1alpha1.ExternalToolList{}
  122. if err := c.List(ctx, etList, client.InNamespace(namespace)); err != nil {
  123. log.Error(err, "Failed to list ExternalTools for Secret event", "namespace", namespace, "secret", obj.GetName())
  124. return nil
  125. }
  126. for i := range etList.Items {
  127. addRef("ExternalTool", etList.Items[i].Name, etList.Items[i].Spec.Auth)
  128. }
  129. mtList := &v1alpha1.ManagedToolList{}
  130. if err := c.List(ctx, mtList, client.InNamespace(namespace)); err != nil {
  131. log.Error(err, "Failed to list ManagedTools for Secret event", "namespace", namespace, "secret", obj.GetName())
  132. return nil
  133. }
  134. for i := range mtList.Items {
  135. addRef("ManagedTool", mtList.Items[i].Name, mtList.Items[i].Spec.Auth)
  136. }
  137. if len(refs) == 0 {
  138. return nil
  139. }
  140. return dependentComponentRequests(ctx, c, namespace, func(comp *v1alpha1.Component) bool {
  141. for kind, names := range refs {
  142. for name := range names {
  143. if componentDependsOn(comp, kind, name, namespace) {
  144. return true
  145. }
  146. }
  147. }
  148. return false
  149. })
  150. }
  151. }
  152. // dependentComponentRequests enqueues every Component in the namespace that match reports
  153. // as depending on the object that triggered the event.
  154. func dependentComponentRequests(ctx context.Context, c client.Client, namespace string, match func(*v1alpha1.Component) bool) []reconcile.Request {
  155. log := logf.FromContext(ctx)
  156. compList := &v1alpha1.ComponentList{}
  157. if err := c.List(ctx, compList, client.InNamespace(namespace)); err != nil {
  158. log.Error(err, "Failed to list Components for dependency event", "namespace", namespace)
  159. return nil
  160. }
  161. var reqs []reconcile.Request
  162. for i := range compList.Items {
  163. comp := &compList.Items[i]
  164. if !match(comp) {
  165. continue
  166. }
  167. reqs = append(reqs, reconcile.Request{
  168. NamespacedName: types.NamespacedName{Name: comp.GetName(), Namespace: comp.GetNamespace()},
  169. })
  170. }
  171. return reqs
  172. }
  173. // componentDependsOn reports whether comp declares the named object as a dependency. A
  174. // dependency reference with no namespace means the Component's own namespace, which is the
  175. // only namespace this is ever asked about.
  176. func componentDependsOn(comp *v1alpha1.Component, kind, name, namespace string) bool {
  177. for _, dep := range comp.Spec.Dependencies {
  178. if dep.Name != name || !dependencyRefMatches(dep, kind) {
  179. continue
  180. }
  181. if dep.Namespace == "" || dep.Namespace == namespace {
  182. return true
  183. }
  184. }
  185. return false
  186. }
  187. // authReadsSecret reports whether auth sources any of its credentials from the named Secret.
  188. func authReadsSecret(auth *v1alpha1.AuthSpec, name string) bool {
  189. for _, ref := range reconciler.AuthSecretRefs(auth) {
  190. if ref.Name == name {
  191. return true
  192. }
  193. }
  194. return false
  195. }