| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- /*
- Copyright 2026 LocoStack.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package reconciler
- import (
- "context"
- "fmt"
- "strings"
- "github.com/LocoStack/loco-operator/api/v1alpha1"
- corev1 "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/runtime"
- "sigs.k8s.io/controller-runtime/pkg/client"
- )
- type ComponentReconciler interface {
- // create all resources for the component, and return the list of dependencies
- ReconcileComponent(ctx context.Context, tmpl *v1alpha1.Template, variables map[string]string) ([]client.Object, error)
- ComponentAvailable(ctx context.Context) bool
- ResourceName(name string) string
- }
- func ResourceLabels(stack, component, name, namespace string) map[string]string {
- labels := map[string]string{
- "app.kubernetes.io/managed-by": "loco-operator",
- "locostack.com/stack": stack,
- "locostack.com/component": component,
- "locostack.com/name": name,
- }
- if namespace != "" {
- labels["locostack.com/namespace"] = namespace
- }
- return labels
- }
- // substituteVariables replaces placeholders in the input string with actual values from the variables map.
- // Placeholders are in the format $(placeholder).
- func substituteVariables(variables map[string]string, input string) (bool, string) {
- for placeholder, value := range variables {
- if !strings.Contains(input, "$(") {
- return true, input
- }
- if strings.Contains(input, fmt.Sprintf("$(%s)", placeholder)) {
- input = strings.ReplaceAll(input, fmt.Sprintf("$(%s)", placeholder), value)
- }
- }
- return false, input
- }
- func mustSubstituteVariables(variables map[string]string, input string) string {
- _, substituted := substituteVariables(variables, input)
- return substituted
- }
- func buildVolumes(variables map[string]string, tmpl *v1alpha1.Template) []corev1.Volume {
- volumes := make([]corev1.Volume, len(tmpl.Spec.Volumes))
- if len(volumes) == 0 {
- volumes = nil
- } else {
- for i, vol := range tmpl.Spec.Volumes {
- volumes[i] = vol
- if vol.VolumeSource.ConfigMap != nil {
- volumes[i].VolumeSource.ConfigMap.LocalObjectReference.Name = mustSubstituteVariables(variables, vol.VolumeSource.ConfigMap.LocalObjectReference.Name)
- }
- if vol.VolumeSource.Secret != nil {
- volumes[i].VolumeSource.Secret.SecretName = mustSubstituteVariables(variables, vol.VolumeSource.Secret.SecretName)
- }
- if vol.VolumeSource.PersistentVolumeClaim != nil {
- volumes[i].VolumeSource.PersistentVolumeClaim.ClaimName = mustSubstituteVariables(variables, vol.VolumeSource.PersistentVolumeClaim.ClaimName)
- }
- }
- }
- return volumes
- }
- func buildContainerSpec(variables map[string]string, tmpl *v1alpha1.Template) corev1.Container {
- command := make([]string, len(tmpl.Spec.Runtime.Command))
- if len(command) == 0 {
- command = nil
- } else {
- for i, cmd := range tmpl.Spec.Runtime.Command {
- command[i] = mustSubstituteVariables(variables, cmd)
- }
- }
- args := make([]string, len(tmpl.Spec.Runtime.Args))
- if len(args) == 0 {
- args = nil
- } else {
- for i, arg := range tmpl.Spec.Runtime.Args {
- args[i] = mustSubstituteVariables(variables, arg)
- }
- for _, arg := range tmpl.Spec.Runtime.ExtraArgs {
- args = append(args, mustSubstituteVariables(variables, arg))
- }
- for _, arg := range tmpl.Spec.Runtime.ConditionalArgs {
- complete, substituted := substituteVariables(variables, arg.When)
- if complete && substituted != "" {
- for _, a := range arg.Args {
- args = append(args, mustSubstituteVariables(variables, a))
- }
- }
- }
- }
- env := make([]corev1.EnvVar, len(tmpl.Spec.Runtime.Env))
- if len(env) == 0 {
- env = nil
- } else {
- for i, envVar := range tmpl.Spec.Runtime.Env {
- env[i] = envVar
- env[i].Value = mustSubstituteVariables(variables, envVar.Value)
- if envVar.ValueFrom != nil && envVar.ValueFrom.SecretKeyRef != nil {
- env[i].ValueFrom.SecretKeyRef.LocalObjectReference.Name = mustSubstituteVariables(variables, envVar.ValueFrom.SecretKeyRef.LocalObjectReference.Name)
- env[i].ValueFrom.SecretKeyRef.Key = mustSubstituteVariables(variables, envVar.ValueFrom.SecretKeyRef.Key)
- }
- }
- }
- envFrom := make([]corev1.EnvFromSource, len(tmpl.Spec.Runtime.EnvFrom))
- if len(envFrom) == 0 {
- envFrom = nil
- } else {
- for i, envFromSource := range tmpl.Spec.Runtime.EnvFrom {
- envFrom[i] = envFromSource
- if envFromSource.ConfigMapRef != nil {
- envFrom[i].ConfigMapRef.LocalObjectReference.Name = mustSubstituteVariables(variables, envFromSource.ConfigMapRef.LocalObjectReference.Name)
- }
- if envFromSource.SecretRef != nil {
- envFrom[i].SecretRef.LocalObjectReference.Name = mustSubstituteVariables(variables, envFromSource.SecretRef.LocalObjectReference.Name)
- }
- }
- }
- ports := make([]corev1.ContainerPort, 0)
- if tmpl.Spec.Runtime.Port != 0 {
- ports = append(ports, corev1.ContainerPort{
- ContainerPort: tmpl.Spec.Runtime.Port,
- Protocol: corev1.ProtocolTCP,
- })
- }
- return corev1.Container{
- Name: "main",
- Image: tmpl.Spec.Runtime.Image,
- ImagePullPolicy: corev1.PullIfNotPresent,
- Command: command,
- Args: args,
- Env: env,
- EnvFrom: envFrom,
- VolumeMounts: tmpl.Spec.Runtime.VolumeMounts,
- Ports: ports,
- Resources: tmpl.Spec.Resources,
- }
- }
- func buildPodSpec(variables map[string]string, tmpl *v1alpha1.Template) (corev1.PodSpec, error) {
- return corev1.PodSpec{
- NodeSelector: tmpl.Spec.NodeSelector,
- Affinity: tmpl.Spec.Affinity,
- Tolerations: tmpl.Spec.Tolerations,
- TopologySpreadConstraints: tmpl.Spec.TopologySpreadConstraints,
- Containers: []corev1.Container{buildContainerSpec(variables, tmpl)},
- SecurityContext: tmpl.Spec.SecurityContext,
- Volumes: buildVolumes(variables, tmpl),
- }, nil
- }
- type EndpointResolver struct {
- client client.Client
- scheme *runtime.Scheme
- namespace string
- }
- func NewEndpointResolver(client client.Client, scheme *runtime.Scheme, namespace string) *EndpointResolver {
- return &EndpointResolver{
- client: client,
- scheme: scheme,
- namespace: namespace,
- }
- }
- func (r *EndpointResolver) ResolveEndpoint(ctx context.Context, stack *v1alpha1.Stack, ref corev1.ObjectReference) (client.Object, string, error) {
- switch ref.Kind {
- case "ExternalModel":
- return r.resolveExternalModelEndpoint(ctx, stack, ref.Name)
- case "ManagedModel":
- return r.resolveManagedModelEndpoint(ctx, stack, ref.Name)
- case "ExternalTool":
- return r.resolveExternalToolEndpoint(ctx, stack, ref.Name)
- case "ManagedTool":
- return r.resolveManagedToolEndpoint(ctx, stack, ref.Name)
- case "ExternalKnowledgeBase":
- return r.resolveExternalKnowledgeBaseEndpoint(ctx, ref.Name)
- case "ManagedKnowledgeBase":
- return r.resolveManagedKnowledgeBaseEndpoint(ctx, ref.Name)
- case "Component":
- return r.resolveComponentEndpoint(ctx, ref.Name)
- default:
- return nil, "", fmt.Errorf("unsupported reference kind: %s", ref.Kind)
- }
- }
- func (r *EndpointResolver) resolveGatewayEndpoint(ctx context.Context, stack *v1alpha1.Stack) (*v1alpha1.Component, error) {
- gatewayEnabled := true
- if stack.Spec.Gateway != nil {
- gatewayEnabled = stack.Spec.Gateway.Enabled
- }
- if !gatewayEnabled {
- return nil, nil
- }
- gwList := &v1alpha1.ComponentList{}
- err := r.client.List(ctx, gwList, client.InNamespace(stack.Namespace), client.MatchingLabels{"locostack.com/component": "Gateway", "stack.locostack.com/default": "true"})
- if err != nil {
- return nil, fmt.Errorf("Failed to list gateway components: %v", err)
- }
- if len(gwList.Items) == 0 {
- return nil, fmt.Errorf("No gateway component found for stack %s", stack.Name)
- } else if len(gwList.Items) > 1 {
- return nil, fmt.Errorf("Multiple gateway components found for stack %s", stack.Name)
- }
- gatewayComp := &gwList.Items[0]
- if gatewayComp.Status.Endpoint == "" {
- return nil, fmt.Errorf("Gateway component for stack %s not ready", stack.Name)
- }
- return gatewayComp, nil
- }
- func (r *EndpointResolver) resolveExternalModelEndpoint(ctx context.Context, stack *v1alpha1.Stack, name string) (client.Object, string, error) {
- var model v1alpha1.ExternalModel
- if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, &model); err != nil {
- return nil, "", fmt.Errorf("Failed to get ExternalModel %s: %w", name, err)
- }
- gatewayComp, err := r.resolveGatewayEndpoint(ctx, stack)
- if err != nil {
- return nil, "", fmt.Errorf("Failed to resolve gateway endpoint for ExternalModel %s: %w", name, err)
- }
- if gatewayComp != nil {
- return &model, gatewayComp.Status.Endpoint, nil
- }
- return &model, model.Spec.APIBase, nil
- }
- func (r *EndpointResolver) resolveManagedModelEndpoint(ctx context.Context, stack *v1alpha1.Stack, name string) (client.Object, string, error) {
- var model v1alpha1.ManagedModel
- if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, &model); err != nil {
- return nil, "", fmt.Errorf("Failed to get ManagedModel %s: %w", name, err)
- }
- gatewayComp, err := r.resolveGatewayEndpoint(ctx, stack)
- if err != nil {
- return nil, "", fmt.Errorf("Failed to resolve gateway endpoint for ManagedModel %s: %w", name, err)
- }
- if gatewayComp != nil {
- return &model, gatewayComp.Status.Endpoint + "/v1", nil
- }
- return &model, model.Status.Endpoint, nil
- }
- func (r *EndpointResolver) resolveExternalToolEndpoint(ctx context.Context, stack *v1alpha1.Stack, name string) (client.Object, string, error) {
- var tool v1alpha1.ExternalTool
- if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, &tool); err != nil {
- return nil, "", fmt.Errorf("Failed to get ExternalTool %s: %w", name, err)
- }
- gatewayComp, err := r.resolveGatewayEndpoint(ctx, stack)
- if err != nil {
- return nil, "", fmt.Errorf("Failed to resolve gateway endpoint for ExternalTool %s: %w", name, err)
- }
- if gatewayComp != nil {
- return &tool, gatewayComp.Status.Endpoint + "/mcp", nil
- }
- return &tool, tool.Spec.Endpoint, nil
- }
- func (r *EndpointResolver) resolveManagedToolEndpoint(ctx context.Context, stack *v1alpha1.Stack, name string) (client.Object, string, error) {
- var tool v1alpha1.ManagedTool
- if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, &tool); err != nil {
- return nil, "", fmt.Errorf("Failed to get ManagedTool %s: %w", name, err)
- }
- gatewayComp, err := r.resolveGatewayEndpoint(ctx, stack)
- if err != nil {
- return nil, "", fmt.Errorf("Failed to resolve gateway endpoint for ManagedTool %s: %w", name, err)
- }
- if gatewayComp != nil {
- return &tool, gatewayComp.Status.Endpoint, nil
- }
- return &tool, tool.Status.Endpoint, nil
- }
- func (r *EndpointResolver) resolveExternalKnowledgeBaseEndpoint(ctx context.Context, name string) (client.Object, string, error) {
- var kb v1alpha1.ExternalKnowledgeBase
- if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, &kb); err != nil {
- return nil, "", fmt.Errorf("Failed to get ExternalKnowledgeBase %s: %w", name, err)
- }
- // TODO: use gateway endpoint
- return &kb, "", nil
- }
- func (r *EndpointResolver) resolveManagedKnowledgeBaseEndpoint(ctx context.Context, name string) (client.Object, string, error) {
- var kb v1alpha1.ManagedKnowledgeBase
- if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, &kb); err != nil {
- return nil, "", fmt.Errorf("Failed to get ManagedKnowledgeBase %s: %w", name, err)
- }
- return &kb, kb.Status.Endpoint, nil
- }
- func (r *EndpointResolver) resolveComponentEndpoint(ctx context.Context, name string) (client.Object, string, error) {
- comp := &v1alpha1.Component{}
- if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: name}, comp); err != nil {
- return nil, "", fmt.Errorf("Failed to get Component %s: %w", name, err)
- }
- return comp, comp.Status.Endpoint, nil
- }
|