component.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  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. "crypto/sha256"
  17. "encoding/hex"
  18. "fmt"
  19. "maps"
  20. "path"
  21. "strings"
  22. "github.com/LocoStack/loco-operator/api/v1alpha1"
  23. "go.yaml.in/yaml/v2"
  24. appsv1 "k8s.io/api/apps/v1"
  25. corev1 "k8s.io/api/core/v1"
  26. discoveryv1 "k8s.io/api/discovery/v1"
  27. "k8s.io/apimachinery/pkg/api/resource"
  28. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  29. "k8s.io/apimachinery/pkg/runtime"
  30. "k8s.io/apimachinery/pkg/util/intstr"
  31. "sigs.k8s.io/controller-runtime/pkg/client"
  32. "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
  33. )
  34. type DefaultComponentReconciler struct {
  35. client client.Client
  36. scheme *runtime.Scheme
  37. stack *v1alpha1.Stack
  38. component *v1alpha1.Component
  39. }
  40. func NewDefaultComponentReconciler(client client.Client, scheme *runtime.Scheme, stack *v1alpha1.Stack, component *v1alpha1.Component) *DefaultComponentReconciler {
  41. reconciler := &DefaultComponentReconciler{
  42. client: client,
  43. scheme: scheme,
  44. stack: stack,
  45. component: component,
  46. }
  47. return reconciler
  48. }
  49. func (r *DefaultComponentReconciler) ReconcileComponent(ctx context.Context, tmpl *v1alpha1.Template, variables map[string]string) ([]client.Object, error) {
  50. r.component.Labels = r.ResourceLabels()
  51. tmplVars := map[string]string{
  52. "metadata.name": r.ResourceName(""),
  53. "spec.runtime.port": fmt.Sprintf("%d", tmpl.Spec.Runtime.Port),
  54. }
  55. maps.Copy(tmplVars, variables)
  56. maps.Copy(tmplVars, r.component.Spec.Variables)
  57. err := r.reconcileService(ctx, tmpl)
  58. if err != nil {
  59. return nil, fmt.Errorf("Failed to reconcile service: %w", err)
  60. }
  61. err = r.reconcileDeployment(ctx, tmpl, tmplVars)
  62. if err != nil {
  63. return nil, fmt.Errorf("Failed to reconcile deployment: %w", err)
  64. }
  65. return nil, nil
  66. }
  67. func (r *DefaultComponentReconciler) ComponentAvailable(ctx context.Context) bool {
  68. endpoints := &discoveryv1.EndpointSliceList{}
  69. err := r.client.List(ctx, endpoints, client.InNamespace(r.component.GetNamespace()), client.MatchingLabels{"kubernetes.io/service-name": r.component.Name})
  70. if err != nil {
  71. return false
  72. }
  73. for _, endpoint := range endpoints.Items {
  74. if len(endpoint.Endpoints) > 0 {
  75. return true
  76. }
  77. }
  78. return false
  79. }
  80. func (r *DefaultComponentReconciler) ResourceName(name string) string {
  81. if name == "" {
  82. return r.component.GetName()
  83. }
  84. return fmt.Sprintf("%s-%s", r.component.GetName(), name)
  85. }
  86. func (r *DefaultComponentReconciler) ResourceLabels() map[string]string {
  87. return ResourceLabels(r.stack.Name, r.component.Spec.Category, r.ResourceName(""), "")
  88. }
  89. func (r *DefaultComponentReconciler) ResourceLabelsWithNamespace() map[string]string {
  90. return ResourceLabels(r.stack.Name, r.component.Spec.Category, r.ResourceName(""), r.component.GetNamespace())
  91. }
  92. func (r *DefaultComponentReconciler) reconcileService(ctx context.Context, tmpl *v1alpha1.Template) error {
  93. labels := r.ResourceLabels()
  94. svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName(""), Namespace: r.component.GetNamespace()}}
  95. _, err := controllerutil.CreateOrUpdate(ctx, r.client, svc, func() error {
  96. svc.Labels = labels
  97. svc.Spec.Type = corev1.ServiceTypeLoadBalancer
  98. svc.Spec.Selector = labels
  99. svc.Spec.Ports = []corev1.ServicePort{{
  100. Port: tmpl.Spec.Runtime.Port,
  101. TargetPort: intstr.FromInt32(tmpl.Spec.Runtime.Port),
  102. Protocol: corev1.ProtocolTCP,
  103. }}
  104. return controllerutil.SetControllerReference(r.component, svc, r.scheme)
  105. })
  106. return err
  107. }
  108. func (r *DefaultComponentReconciler) reconcileDeployment(ctx context.Context, tmpl *v1alpha1.Template, variables map[string]string) error {
  109. podSpec, err := buildPodSpec(variables, tmpl)
  110. if err != nil {
  111. return fmt.Errorf("Failed to build pod spec: %w", err)
  112. }
  113. labels := r.ResourceLabels()
  114. maps.Copy(labels, tmpl.Metadata.Labels)
  115. replicas := int32(1)
  116. dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName(""), Namespace: r.component.GetNamespace()}}
  117. _, err = controllerutil.CreateOrUpdate(ctx, r.client, dep, func() error {
  118. dep.Labels = labels
  119. dep.Spec.Replicas = &replicas
  120. dep.Spec.Selector = &metav1.LabelSelector{MatchLabels: labels}
  121. dep.Spec.Template = corev1.PodTemplateSpec{
  122. ObjectMeta: metav1.ObjectMeta{
  123. Labels: labels,
  124. Annotations: tmpl.Metadata.Annotations,
  125. },
  126. Spec: podSpec,
  127. }
  128. return controllerutil.SetControllerReference(r.component, dep, r.scheme)
  129. })
  130. return err
  131. }
  132. func (r *DefaultComponentReconciler) ReconcileStorage(ctx context.Context, name, size string) error {
  133. pvName, err := r.reconcilePersistentVolume(ctx, name, size)
  134. if err != nil {
  135. return fmt.Errorf("Failed to reconcile persistent volume: %w", err)
  136. }
  137. if err := r.reconcilePersistentVolumeClaim(ctx, pvName, name, size); err != nil {
  138. return fmt.Errorf("Failed to reconcile persistent volume claim: %w", err)
  139. }
  140. return nil
  141. }
  142. func (r *DefaultComponentReconciler) reconcilePersistentVolume(ctx context.Context, name, size string) (string, error) {
  143. pvName := fmt.Sprintf("%s-%s", r.component.GetNamespace(), r.ResourceName(name))
  144. pv := &corev1.PersistentVolume{ObjectMeta: metav1.ObjectMeta{Name: pvName}}
  145. storage := resource.MustParse(size)
  146. hostPathType := corev1.HostPathDirectoryOrCreate
  147. hostPath := path.Join("/var/lib/locostack", r.component.GetNamespace(), r.ResourceName(""))
  148. _, err := controllerutil.CreateOrUpdate(ctx, r.client, pv, func() error {
  149. if pv.Labels == nil {
  150. pv.Labels = map[string]string{}
  151. }
  152. maps.Copy(pv.Labels, r.ResourceLabelsWithNamespace())
  153. pv.Spec.Capacity = corev1.ResourceList{corev1.ResourceStorage: storage}
  154. pv.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}
  155. pv.Spec.PersistentVolumeReclaimPolicy = corev1.PersistentVolumeReclaimRetain
  156. pv.Spec.StorageClassName = ""
  157. pv.Spec.VolumeMode = nil
  158. pv.Spec.PersistentVolumeSource = corev1.PersistentVolumeSource{
  159. HostPath: &corev1.HostPathVolumeSource{
  160. Path: hostPath,
  161. Type: &hostPathType,
  162. },
  163. }
  164. return nil
  165. })
  166. return pvName, err
  167. }
  168. func (r *DefaultComponentReconciler) reconcilePersistentVolumeClaim(ctx context.Context, pvName, name, size string) error {
  169. pvc := &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName(name), Namespace: r.component.GetNamespace()}}
  170. storage := resource.MustParse(size)
  171. emptyStorageClass := ""
  172. _, err := controllerutil.CreateOrUpdate(ctx, r.client, pvc, func() error {
  173. if pvc.Labels == nil {
  174. pvc.Labels = map[string]string{}
  175. }
  176. maps.Copy(pvc.Labels, r.ResourceLabels())
  177. pvc.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}
  178. pvc.Spec.VolumeName = pvName
  179. pvc.Spec.StorageClassName = &emptyStorageClass
  180. if pvc.Spec.Resources.Requests == nil {
  181. pvc.Spec.Resources.Requests = corev1.ResourceList{}
  182. }
  183. pvc.Spec.Resources.Requests[corev1.ResourceStorage] = storage
  184. return nil
  185. })
  186. return err
  187. }
  188. func (r *DefaultComponentReconciler) ReconcileKey(ctx context.Context, name, key string, keygen func() (string, error)) error {
  189. secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName(name), Namespace: r.component.GetNamespace()}}
  190. _, err := controllerutil.CreateOrUpdate(ctx, r.client, secret, func() error {
  191. if secret.Labels == nil {
  192. secret.Labels = map[string]string{}
  193. }
  194. maps.Copy(secret.Labels, r.ResourceLabels())
  195. if secret.Data == nil {
  196. value, err := keygen()
  197. if err != nil {
  198. return err
  199. }
  200. secret.Data = map[string][]byte{key: []byte(value)}
  201. }
  202. return controllerutil.SetControllerReference(r.component, secret, r.scheme)
  203. })
  204. return err
  205. }
  206. func (r *DefaultComponentReconciler) ReconcileConfig(ctx context.Context, key string, config map[string]string, isSecret bool) (string, error) {
  207. if err := r.reconcileEnvVarConfig(ctx, config, isSecret); err != nil {
  208. return "", fmt.Errorf("Failed to reconcile env var config map: %w", err)
  209. }
  210. configHash, err := r.reconcileFileConfig(ctx, key, config, isSecret)
  211. if err != nil {
  212. return "", fmt.Errorf("Failed to reconcile file config map: %w", err)
  213. }
  214. return configHash, nil
  215. }
  216. func (r *DefaultComponentReconciler) reconcileEnvVarConfig(ctx context.Context, config map[string]string, isSecret bool) error {
  217. configData := make(map[string]string)
  218. for k, v := range config {
  219. envVarName := strings.NewReplacer("-", "_", ".", "_").Replace(k)
  220. configData[strings.ToUpper(envVarName)] = v
  221. }
  222. var obj client.Object
  223. if isSecret {
  224. obj = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName("env"), Namespace: r.component.GetNamespace()}}
  225. } else {
  226. obj = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName("env"), Namespace: r.component.GetNamespace()}}
  227. }
  228. _, err := controllerutil.CreateOrUpdate(ctx, r.client, obj, func() error {
  229. if obj.GetLabels() == nil {
  230. obj.SetLabels(map[string]string{})
  231. }
  232. maps.Copy(obj.GetLabels(), r.ResourceLabels())
  233. switch o := obj.(type) {
  234. case *corev1.ConfigMap:
  235. o.Data = configData
  236. case *corev1.Secret:
  237. o.Data = make(map[string][]byte)
  238. for k, v := range configData {
  239. o.Data[k] = []byte(v)
  240. }
  241. }
  242. return controllerutil.SetControllerReference(r.component, obj, r.scheme)
  243. })
  244. return err
  245. }
  246. func (r *DefaultComponentReconciler) reconcileFileConfig(ctx context.Context, key string, config map[string]string, isSecret bool) (string, error) {
  247. yamlConfig, err := yaml.Marshal(config)
  248. if err != nil {
  249. return "", fmt.Errorf("Failed to marshal config to yaml: %w", err)
  250. }
  251. h := sha256.Sum256(yamlConfig)
  252. configHash := hex.EncodeToString(h[:])
  253. var obj client.Object
  254. if isSecret {
  255. obj = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName("config"), Namespace: r.component.GetNamespace()}}
  256. } else {
  257. obj = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName("config"), Namespace: r.component.GetNamespace()}}
  258. }
  259. _, err = controllerutil.CreateOrUpdate(ctx, r.client, obj, func() error {
  260. if obj.GetLabels() == nil {
  261. obj.SetLabels(map[string]string{})
  262. }
  263. maps.Copy(obj.GetLabels(), r.ResourceLabels())
  264. if obj.GetAnnotations() == nil {
  265. obj.SetAnnotations(map[string]string{})
  266. }
  267. obj.GetAnnotations()["hash"] = configHash
  268. switch o := obj.(type) {
  269. case *corev1.ConfigMap:
  270. o.Data = map[string]string{
  271. key: string(yamlConfig),
  272. }
  273. case *corev1.Secret:
  274. o.Data = map[string][]byte{
  275. key: yamlConfig,
  276. }
  277. }
  278. return controllerutil.SetControllerReference(r.component, obj, r.scheme)
  279. })
  280. return configHash, err
  281. }