common.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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 reconciler
  14. import (
  15. "context"
  16. "fmt"
  17. "strings"
  18. "github.com/LocoStack/loco-operator/api/v1alpha1"
  19. corev1 "k8s.io/api/core/v1"
  20. "k8s.io/apimachinery/pkg/runtime"
  21. "sigs.k8s.io/controller-runtime/pkg/client"
  22. )
  23. type ComponentReconciler interface {
  24. // create all resources for the component, and return the list of dependencies
  25. ReconcileComponent(ctx context.Context, tmpl *v1alpha1.Template, variables map[string]string) ([]client.Object, error)
  26. ComponentAvailable(ctx context.Context) bool
  27. ResourceName(name string) string
  28. }
  29. func ResourceLabels(stack, component, name, namespace string) map[string]string {
  30. labels := map[string]string{
  31. "app.kubernetes.io/managed-by": "loco-operator",
  32. "locostack.com/stack": stack,
  33. "locostack.com/component": component,
  34. "locostack.com/name": name,
  35. }
  36. if namespace != "" {
  37. labels["locostack.com/namespace"] = namespace
  38. }
  39. return labels
  40. }
  41. // substituteVariables replaces placeholders in the input string with actual values from the variables map.
  42. // Placeholders are in the format $(placeholder).
  43. func substituteVariables(variables map[string]string, input string) (bool, string) {
  44. for placeholder, value := range variables {
  45. if !strings.Contains(input, "$(") {
  46. return true, input
  47. }
  48. if strings.Contains(input, fmt.Sprintf("$(%s)", placeholder)) {
  49. input = strings.ReplaceAll(input, fmt.Sprintf("$(%s)", placeholder), value)
  50. }
  51. }
  52. return false, input
  53. }
  54. func mustSubstituteVariables(variables map[string]string, input string) string {
  55. _, substituted := substituteVariables(variables, input)
  56. return substituted
  57. }
  58. func buildVolumes(variables map[string]string, tmpl *v1alpha1.Template) []corev1.Volume {
  59. volumes := make([]corev1.Volume, len(tmpl.Spec.Volumes))
  60. if len(volumes) == 0 {
  61. volumes = nil
  62. } else {
  63. for i, vol := range tmpl.Spec.Volumes {
  64. volumes[i] = vol
  65. if vol.VolumeSource.ConfigMap != nil {
  66. volumes[i].VolumeSource.ConfigMap.LocalObjectReference.Name = mustSubstituteVariables(variables, vol.VolumeSource.ConfigMap.LocalObjectReference.Name)
  67. }
  68. if vol.VolumeSource.Secret != nil {
  69. volumes[i].VolumeSource.Secret.SecretName = mustSubstituteVariables(variables, vol.VolumeSource.Secret.SecretName)
  70. }
  71. if vol.VolumeSource.PersistentVolumeClaim != nil {
  72. volumes[i].VolumeSource.PersistentVolumeClaim.ClaimName = mustSubstituteVariables(variables, vol.VolumeSource.PersistentVolumeClaim.ClaimName)
  73. }
  74. }
  75. }
  76. return volumes
  77. }
  78. func buildContainerSpec(variables map[string]string, tmpl *v1alpha1.Template) corev1.Container {
  79. command := make([]string, len(tmpl.Spec.Runtime.Command))
  80. if len(command) == 0 {
  81. command = nil
  82. } else {
  83. for i, cmd := range tmpl.Spec.Runtime.Command {
  84. command[i] = mustSubstituteVariables(variables, cmd)
  85. }
  86. }
  87. args := make([]string, len(tmpl.Spec.Runtime.Args))
  88. if len(args) == 0 {
  89. args = nil
  90. } else {
  91. for i, arg := range tmpl.Spec.Runtime.Args {
  92. args[i] = mustSubstituteVariables(variables, arg)
  93. }
  94. for _, arg := range tmpl.Spec.Runtime.ExtraArgs {
  95. args = append(args, mustSubstituteVariables(variables, arg))
  96. }
  97. for _, arg := range tmpl.Spec.Runtime.ConditionalArgs {
  98. complete, substituted := substituteVariables(variables, arg.When)
  99. if complete && substituted != "" {
  100. for _, a := range arg.Args {
  101. args = append(args, mustSubstituteVariables(variables, a))
  102. }
  103. }
  104. }
  105. }
  106. env := make([]corev1.EnvVar, len(tmpl.Spec.Runtime.Env))
  107. if len(env) == 0 {
  108. env = nil
  109. } else {
  110. for i, envVar := range tmpl.Spec.Runtime.Env {
  111. env[i] = envVar
  112. env[i].Value = mustSubstituteVariables(variables, envVar.Value)
  113. if envVar.ValueFrom != nil && envVar.ValueFrom.SecretKeyRef != nil {
  114. env[i].ValueFrom.SecretKeyRef.LocalObjectReference.Name = mustSubstituteVariables(variables, envVar.ValueFrom.SecretKeyRef.LocalObjectReference.Name)
  115. env[i].ValueFrom.SecretKeyRef.Key = mustSubstituteVariables(variables, envVar.ValueFrom.SecretKeyRef.Key)
  116. }
  117. }
  118. }
  119. envFrom := make([]corev1.EnvFromSource, len(tmpl.Spec.Runtime.EnvFrom))
  120. if len(envFrom) == 0 {
  121. envFrom = nil
  122. } else {
  123. for i, envFromSource := range tmpl.Spec.Runtime.EnvFrom {
  124. envFrom[i] = envFromSource
  125. if envFromSource.ConfigMapRef != nil {
  126. envFrom[i].ConfigMapRef.LocalObjectReference.Name = mustSubstituteVariables(variables, envFromSource.ConfigMapRef.LocalObjectReference.Name)
  127. }
  128. if envFromSource.SecretRef != nil {
  129. envFrom[i].SecretRef.LocalObjectReference.Name = mustSubstituteVariables(variables, envFromSource.SecretRef.LocalObjectReference.Name)
  130. }
  131. }
  132. }
  133. ports := make([]corev1.ContainerPort, 0)
  134. if tmpl.Spec.Runtime.Port != 0 {
  135. ports = append(ports, corev1.ContainerPort{
  136. ContainerPort: tmpl.Spec.Runtime.Port,
  137. Protocol: corev1.ProtocolTCP,
  138. })
  139. }
  140. return corev1.Container{
  141. Name: "main",
  142. Image: tmpl.Spec.Runtime.Image,
  143. ImagePullPolicy: corev1.PullIfNotPresent,
  144. Command: command,
  145. Args: args,
  146. Env: env,
  147. EnvFrom: envFrom,
  148. VolumeMounts: tmpl.Spec.Runtime.VolumeMounts,
  149. Ports: ports,
  150. Resources: tmpl.Spec.Resources,
  151. }
  152. }
  153. func buildPodSpec(variables map[string]string, tmpl *v1alpha1.Template) (corev1.PodSpec, error) {
  154. return corev1.PodSpec{
  155. NodeSelector: tmpl.Spec.NodeSelector,
  156. Affinity: tmpl.Spec.Affinity,
  157. Tolerations: tmpl.Spec.Tolerations,
  158. TopologySpreadConstraints: tmpl.Spec.TopologySpreadConstraints,
  159. Containers: []corev1.Container{buildContainerSpec(variables, tmpl)},
  160. SecurityContext: tmpl.Spec.SecurityContext,
  161. Volumes: buildVolumes(variables, tmpl),
  162. }, nil
  163. }
  164. type EndpointResolver struct {
  165. client client.Client
  166. scheme *runtime.Scheme
  167. namespace string
  168. }
  169. func NewEndpointResolver(client client.Client, scheme *runtime.Scheme, namespace string) *EndpointResolver {
  170. return &EndpointResolver{
  171. client: client,
  172. scheme: scheme,
  173. namespace: namespace,
  174. }
  175. }
  176. func (r *EndpointResolver) ResolveEndpoint(ctx context.Context, stack *v1alpha1.Stack, ref corev1.ObjectReference) (client.Object, string, error) {
  177. switch ref.Kind {
  178. case "ExternalModel":
  179. return r.resolveExternalModelEndpoint(ctx, stack, ref.Name)
  180. case "ManagedModel":
  181. return r.resolveManagedModelEndpoint(ctx, stack, ref.Name)
  182. case "ExternalTool":
  183. return r.resolveExternalToolEndpoint(ctx, stack, ref.Name)
  184. case "ManagedTool":
  185. return r.resolveManagedToolEndpoint(ctx, stack, ref.Name)
  186. case "ExternalKnowledgeBase":
  187. return r.resolveExternalKnowledgeBaseEndpoint(ctx, ref.Name)
  188. case "ManagedKnowledgeBase":
  189. return r.resolveManagedKnowledgeBaseEndpoint(ctx, ref.Name)
  190. case "Component":
  191. return r.resolveComponentEndpoint(ctx, ref.Name)
  192. default:
  193. return nil, "", fmt.Errorf("unsupported reference kind: %s", ref.Kind)
  194. }
  195. }
  196. func (r *EndpointResolver) resolveGatewayEndpoint(ctx context.Context, stack *v1alpha1.Stack) (*v1alpha1.Component, error) {
  197. gatewayEnabled := true
  198. if stack.Spec.Gateway != nil {
  199. gatewayEnabled = stack.Spec.Gateway.Enabled
  200. }
  201. if !gatewayEnabled {
  202. return nil, nil
  203. }
  204. gwList := &v1alpha1.ComponentList{}
  205. err := r.client.List(ctx, gwList, client.InNamespace(stack.Namespace), client.MatchingLabels{"locostack.com/component": "Gateway", "stack.locostack.com/default": "true"})
  206. if err != nil {
  207. return nil, fmt.Errorf("Failed to list gateway components: %v", err)
  208. }
  209. if len(gwList.Items) == 0 {
  210. return nil, fmt.Errorf("No gateway component found for stack %s", stack.Name)
  211. } else if len(gwList.Items) > 1 {
  212. return nil, fmt.Errorf("Multiple gateway components found for stack %s", stack.Name)
  213. }
  214. gatewayComp := &gwList.Items[0]
  215. if gatewayComp.Status.Endpoint == "" {
  216. return nil, fmt.Errorf("Gateway component for stack %s not ready", stack.Name)
  217. }
  218. return gatewayComp, nil
  219. }
  220. func (r *EndpointResolver) resolveExternalModelEndpoint(ctx context.Context, stack *v1alpha1.Stack, name string) (client.Object, string, error) {
  221. var model v1alpha1.ExternalModel
  222. if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, &model); err != nil {
  223. return nil, "", fmt.Errorf("Failed to get ExternalModel %s: %w", name, err)
  224. }
  225. gatewayComp, err := r.resolveGatewayEndpoint(ctx, stack)
  226. if err != nil {
  227. return nil, "", fmt.Errorf("Failed to resolve gateway endpoint for ExternalModel %s: %w", name, err)
  228. }
  229. if gatewayComp != nil {
  230. return &model, gatewayComp.Status.Endpoint, nil
  231. }
  232. return &model, model.Spec.APIBase, nil
  233. }
  234. func (r *EndpointResolver) resolveManagedModelEndpoint(ctx context.Context, stack *v1alpha1.Stack, name string) (client.Object, string, error) {
  235. var model v1alpha1.ManagedModel
  236. if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, &model); err != nil {
  237. return nil, "", fmt.Errorf("Failed to get ManagedModel %s: %w", name, err)
  238. }
  239. gatewayComp, err := r.resolveGatewayEndpoint(ctx, stack)
  240. if err != nil {
  241. return nil, "", fmt.Errorf("Failed to resolve gateway endpoint for ManagedModel %s: %w", name, err)
  242. }
  243. if gatewayComp != nil {
  244. return &model, gatewayComp.Status.Endpoint + "/v1", nil
  245. }
  246. return &model, model.Status.Endpoint, nil
  247. }
  248. func (r *EndpointResolver) resolveExternalToolEndpoint(ctx context.Context, stack *v1alpha1.Stack, name string) (client.Object, string, error) {
  249. var tool v1alpha1.ExternalTool
  250. if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, &tool); err != nil {
  251. return nil, "", fmt.Errorf("Failed to get ExternalTool %s: %w", name, err)
  252. }
  253. gatewayComp, err := r.resolveGatewayEndpoint(ctx, stack)
  254. if err != nil {
  255. return nil, "", fmt.Errorf("Failed to resolve gateway endpoint for ExternalTool %s: %w", name, err)
  256. }
  257. if gatewayComp != nil {
  258. return &tool, gatewayComp.Status.Endpoint + "/mcp", nil
  259. }
  260. return &tool, tool.Spec.Endpoint, nil
  261. }
  262. func (r *EndpointResolver) resolveManagedToolEndpoint(ctx context.Context, stack *v1alpha1.Stack, name string) (client.Object, string, error) {
  263. var tool v1alpha1.ManagedTool
  264. if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, &tool); err != nil {
  265. return nil, "", fmt.Errorf("Failed to get ManagedTool %s: %w", name, err)
  266. }
  267. gatewayComp, err := r.resolveGatewayEndpoint(ctx, stack)
  268. if err != nil {
  269. return nil, "", fmt.Errorf("Failed to resolve gateway endpoint for ManagedTool %s: %w", name, err)
  270. }
  271. if gatewayComp != nil {
  272. return &tool, gatewayComp.Status.Endpoint, nil
  273. }
  274. return &tool, tool.Status.Endpoint, nil
  275. }
  276. func (r *EndpointResolver) resolveExternalKnowledgeBaseEndpoint(ctx context.Context, name string) (client.Object, string, error) {
  277. var kb v1alpha1.ExternalKnowledgeBase
  278. if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, &kb); err != nil {
  279. return nil, "", fmt.Errorf("Failed to get ExternalKnowledgeBase %s: %w", name, err)
  280. }
  281. // TODO: use gateway endpoint
  282. return &kb, "", nil
  283. }
  284. func (r *EndpointResolver) resolveManagedKnowledgeBaseEndpoint(ctx context.Context, name string) (client.Object, string, error) {
  285. var kb v1alpha1.ManagedKnowledgeBase
  286. if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, &kb); err != nil {
  287. return nil, "", fmt.Errorf("Failed to get ManagedKnowledgeBase %s: %w", name, err)
  288. }
  289. return &kb, kb.Status.Endpoint, nil
  290. }
  291. func (r *EndpointResolver) resolveComponentEndpoint(ctx context.Context, name string) (client.Object, string, error) {
  292. comp := &v1alpha1.Component{}
  293. if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, comp); err != nil {
  294. return nil, "", fmt.Errorf("Failed to get Component %s: %w", name, err)
  295. }
  296. return comp, comp.Status.Endpoint, nil
  297. }