common.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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. "sigs.k8s.io/controller-runtime/pkg/client"
  21. )
  22. type ComponentReconciler interface {
  23. // create all resources for the component, and return the list of dependencies
  24. ReconcileComponent(ctx context.Context, tmpl *v1alpha1.Template, variables map[string]string) ([]client.Object, error)
  25. ComponentAvailable(ctx context.Context) bool
  26. ResourceName(name string) string
  27. }
  28. func ResourceLabels(stack, component, name, namespace string) map[string]string {
  29. labels := map[string]string{
  30. "app.kubernetes.io/managed-by": "loco-operator",
  31. "locostack.com/stack": stack,
  32. "locostack.com/component": component,
  33. "locostack.com/name": name,
  34. }
  35. if namespace != "" {
  36. labels["locostack.com/namespace"] = namespace
  37. }
  38. return labels
  39. }
  40. // substituteVariables replaces placeholders in the input string with actual values from the variables map.
  41. // Placeholders are in the format $(placeholder).
  42. func substituteVariables(variables map[string]string, input string) (bool, string) {
  43. for placeholder, value := range variables {
  44. if !strings.Contains(input, "$(") {
  45. return true, input
  46. }
  47. if strings.Contains(input, fmt.Sprintf("$(%s)", placeholder)) {
  48. input = strings.ReplaceAll(input, fmt.Sprintf("$(%s)", placeholder), value)
  49. }
  50. }
  51. return false, input
  52. }
  53. func mustSubstituteVariables(variables map[string]string, input string) string {
  54. _, substituted := substituteVariables(variables, input)
  55. return substituted
  56. }
  57. func buildVolumes(variables map[string]string, tmpl *v1alpha1.Template) []corev1.Volume {
  58. volumes := make([]corev1.Volume, len(tmpl.Spec.Volumes))
  59. if len(volumes) == 0 {
  60. volumes = nil
  61. } else {
  62. for i, vol := range tmpl.Spec.Volumes {
  63. volumes[i] = vol
  64. if vol.VolumeSource.ConfigMap != nil {
  65. volumes[i].VolumeSource.ConfigMap.LocalObjectReference.Name = mustSubstituteVariables(variables, vol.VolumeSource.ConfigMap.LocalObjectReference.Name)
  66. }
  67. if vol.VolumeSource.Secret != nil {
  68. volumes[i].VolumeSource.Secret.SecretName = mustSubstituteVariables(variables, vol.VolumeSource.Secret.SecretName)
  69. }
  70. if vol.VolumeSource.PersistentVolumeClaim != nil {
  71. volumes[i].VolumeSource.PersistentVolumeClaim.ClaimName = mustSubstituteVariables(variables, vol.VolumeSource.PersistentVolumeClaim.ClaimName)
  72. }
  73. }
  74. }
  75. return volumes
  76. }
  77. func buildContainerSpec(variables map[string]string, tmpl *v1alpha1.Template) corev1.Container {
  78. command := make([]string, len(tmpl.Spec.Runtime.Command))
  79. if len(command) == 0 {
  80. command = nil
  81. } else {
  82. for i, cmd := range tmpl.Spec.Runtime.Command {
  83. command[i] = mustSubstituteVariables(variables, cmd)
  84. }
  85. }
  86. args := make([]string, len(tmpl.Spec.Runtime.Args))
  87. if len(args) == 0 {
  88. args = nil
  89. } else {
  90. for i, arg := range tmpl.Spec.Runtime.Args {
  91. args[i] = mustSubstituteVariables(variables, arg)
  92. }
  93. for _, arg := range tmpl.Spec.Runtime.ExtraArgs {
  94. args = append(args, mustSubstituteVariables(variables, arg))
  95. }
  96. for _, arg := range tmpl.Spec.Runtime.ConditionalArgs {
  97. complete, substituted := substituteVariables(variables, arg.When)
  98. if complete && substituted != "" {
  99. for _, a := range arg.Args {
  100. args = append(args, mustSubstituteVariables(variables, a))
  101. }
  102. }
  103. }
  104. }
  105. env := make([]corev1.EnvVar, len(tmpl.Spec.Runtime.Env))
  106. if len(env) == 0 {
  107. env = nil
  108. } else {
  109. for i, envVar := range tmpl.Spec.Runtime.Env {
  110. env[i] = envVar
  111. env[i].Value = mustSubstituteVariables(variables, envVar.Value)
  112. if envVar.ValueFrom != nil && envVar.ValueFrom.SecretKeyRef != nil {
  113. env[i].ValueFrom.SecretKeyRef.LocalObjectReference.Name = mustSubstituteVariables(variables, envVar.ValueFrom.SecretKeyRef.LocalObjectReference.Name)
  114. env[i].ValueFrom.SecretKeyRef.Key = mustSubstituteVariables(variables, envVar.ValueFrom.SecretKeyRef.Key)
  115. }
  116. }
  117. }
  118. envFrom := make([]corev1.EnvFromSource, len(tmpl.Spec.Runtime.EnvFrom))
  119. if len(envFrom) == 0 {
  120. envFrom = nil
  121. } else {
  122. for i, envFromSource := range tmpl.Spec.Runtime.EnvFrom {
  123. envFrom[i] = envFromSource
  124. if envFromSource.ConfigMapRef != nil {
  125. envFrom[i].ConfigMapRef.LocalObjectReference.Name = mustSubstituteVariables(variables, envFromSource.ConfigMapRef.LocalObjectReference.Name)
  126. }
  127. if envFromSource.SecretRef != nil {
  128. envFrom[i].SecretRef.LocalObjectReference.Name = mustSubstituteVariables(variables, envFromSource.SecretRef.LocalObjectReference.Name)
  129. }
  130. }
  131. }
  132. ports := make([]corev1.ContainerPort, 0)
  133. if tmpl.Spec.Runtime.Port != 0 {
  134. ports = append(ports, corev1.ContainerPort{
  135. ContainerPort: tmpl.Spec.Runtime.Port,
  136. Protocol: corev1.ProtocolTCP,
  137. })
  138. }
  139. return corev1.Container{
  140. Name: "main",
  141. Image: tmpl.Spec.Runtime.Image,
  142. ImagePullPolicy: corev1.PullIfNotPresent,
  143. Command: command,
  144. Args: args,
  145. Env: env,
  146. EnvFrom: envFrom,
  147. VolumeMounts: tmpl.Spec.Runtime.VolumeMounts,
  148. Ports: ports,
  149. Resources: tmpl.Spec.Resources,
  150. }
  151. }
  152. func buildPodSpec(variables map[string]string, tmpl *v1alpha1.Template) (corev1.PodSpec, error) {
  153. return corev1.PodSpec{
  154. NodeSelector: tmpl.Spec.NodeSelector,
  155. Affinity: tmpl.Spec.Affinity,
  156. Tolerations: tmpl.Spec.Tolerations,
  157. TopologySpreadConstraints: tmpl.Spec.TopologySpreadConstraints,
  158. Containers: []corev1.Container{buildContainerSpec(variables, tmpl)},
  159. SecurityContext: tmpl.Spec.SecurityContext,
  160. Volumes: buildVolumes(variables, tmpl),
  161. }, nil
  162. }