common.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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. "sort"
  18. "strings"
  19. "github.com/LocoStack/loco-operator/api/v1alpha1"
  20. corev1 "k8s.io/api/core/v1"
  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. func sortedKeys[V any](m map[string]V) []string {
  165. keys := make([]string, 0, len(m))
  166. for key := range m {
  167. keys = append(keys, key)
  168. }
  169. sort.Strings(keys)
  170. return keys
  171. }