component.go 11 KB

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