| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- /*
- 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"
- "crypto/sha256"
- "encoding/hex"
- "fmt"
- "maps"
- "path"
- "strings"
- "github.com/LocoStack/loco-operator/api/v1alpha1"
- "go.yaml.in/yaml/v2"
- appsv1 "k8s.io/api/apps/v1"
- corev1 "k8s.io/api/core/v1"
- discoveryv1 "k8s.io/api/discovery/v1"
- "k8s.io/apimachinery/pkg/api/resource"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/util/intstr"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
- )
- type DefaultComponentReconciler struct {
- client client.Client
- scheme *runtime.Scheme
- stack *v1alpha1.Stack
- component *v1alpha1.Component
- }
- func NewDefaultComponentReconciler(client client.Client, scheme *runtime.Scheme, stack *v1alpha1.Stack, component *v1alpha1.Component) *DefaultComponentReconciler {
- reconciler := &DefaultComponentReconciler{
- client: client,
- scheme: scheme,
- stack: stack,
- component: component,
- }
- return reconciler
- }
- func (r *DefaultComponentReconciler) ReconcileComponent(ctx context.Context, tmpl *v1alpha1.Template, variables map[string]string) ([]client.Object, error) {
- r.component.Labels = r.ResourceLabels()
- tmplVars := map[string]string{
- "metadata.name": r.ResourceName(""),
- "spec.runtime.port": fmt.Sprintf("%d", tmpl.Spec.Runtime.Port),
- }
- maps.Copy(tmplVars, variables)
- maps.Copy(tmplVars, r.component.Spec.Variables)
- err := r.reconcileService(ctx, tmpl)
- if err != nil {
- return nil, fmt.Errorf("Failed to reconcile service: %w", err)
- }
- err = r.reconcileDeployment(ctx, tmpl, tmplVars)
- if err != nil {
- return nil, fmt.Errorf("Failed to reconcile deployment: %w", err)
- }
- return nil, nil
- }
- func (r *DefaultComponentReconciler) ComponentAvailable(ctx context.Context) bool {
- endpoints := &discoveryv1.EndpointSliceList{}
- err := r.client.List(ctx, endpoints, client.InNamespace(r.component.GetNamespace()), client.MatchingLabels{"kubernetes.io/service-name": r.component.Name})
- if err != nil {
- return false
- }
- for _, endpoint := range endpoints.Items {
- if len(endpoint.Endpoints) > 0 {
- return true
- }
- }
- return false
- }
- func (r *DefaultComponentReconciler) ResourceName(name string) string {
- if name == "" {
- return r.component.GetName()
- }
- return fmt.Sprintf("%s-%s", r.component.GetName(), name)
- }
- func (r *DefaultComponentReconciler) ResourceLabels() map[string]string {
- return ResourceLabels(r.stack.Name, r.component.Spec.Category, r.ResourceName(""), "")
- }
- func (r *DefaultComponentReconciler) ResourceLabelsWithNamespace() map[string]string {
- return ResourceLabels(r.stack.Name, r.component.Spec.Category, r.ResourceName(""), r.component.GetNamespace())
- }
- func (r *DefaultComponentReconciler) reconcileService(ctx context.Context, tmpl *v1alpha1.Template) error {
- labels := r.ResourceLabels()
- svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName(""), Namespace: r.component.GetNamespace()}}
- _, err := controllerutil.CreateOrUpdate(ctx, r.client, svc, func() error {
- svc.Labels = labels
- svc.Spec.Type = corev1.ServiceTypeLoadBalancer
- svc.Spec.Selector = labels
- svc.Spec.Ports = []corev1.ServicePort{{
- Port: tmpl.Spec.Runtime.Port,
- TargetPort: intstr.FromInt32(tmpl.Spec.Runtime.Port),
- Protocol: corev1.ProtocolTCP,
- }}
- return controllerutil.SetControllerReference(r.component, svc, r.scheme)
- })
- return err
- }
- func (r *DefaultComponentReconciler) reconcileDeployment(ctx context.Context, tmpl *v1alpha1.Template, variables map[string]string) error {
- podSpec, err := buildPodSpec(variables, tmpl)
- if err != nil {
- return fmt.Errorf("Failed to build pod spec: %w", err)
- }
- labels := r.ResourceLabels()
- maps.Copy(labels, tmpl.Metadata.Labels)
- replicas := int32(1)
- dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName(""), Namespace: r.component.GetNamespace()}}
- _, err = controllerutil.CreateOrUpdate(ctx, r.client, dep, func() error {
- dep.Labels = labels
- dep.Spec.Replicas = &replicas
- dep.Spec.Selector = &metav1.LabelSelector{MatchLabels: labels}
- dep.Spec.Template = corev1.PodTemplateSpec{
- ObjectMeta: metav1.ObjectMeta{
- Labels: labels,
- Annotations: tmpl.Metadata.Annotations,
- },
- Spec: podSpec,
- }
- return controllerutil.SetControllerReference(r.component, dep, r.scheme)
- })
- return err
- }
- func (r *DefaultComponentReconciler) ReconcileStorage(ctx context.Context, name, size string) error {
- pvName, err := r.reconcilePersistentVolume(ctx, name, size)
- if err != nil {
- return fmt.Errorf("Failed to reconcile persistent volume: %w", err)
- }
- if err := r.reconcilePersistentVolumeClaim(ctx, pvName, name, size); err != nil {
- return fmt.Errorf("Failed to reconcile persistent volume claim: %w", err)
- }
- return nil
- }
- func (r *DefaultComponentReconciler) reconcilePersistentVolume(ctx context.Context, name, size string) (string, error) {
- pvName := fmt.Sprintf("%s-%s", r.component.GetNamespace(), r.ResourceName(name))
- pv := &corev1.PersistentVolume{ObjectMeta: metav1.ObjectMeta{Name: pvName}}
- storage := resource.MustParse(size)
- hostPathType := corev1.HostPathDirectoryOrCreate
- hostPath := path.Join("/var/lib/locostack", r.component.GetNamespace(), r.ResourceName(""))
- _, err := controllerutil.CreateOrUpdate(ctx, r.client, pv, func() error {
- if pv.Labels == nil {
- pv.Labels = map[string]string{}
- }
- maps.Copy(pv.Labels, r.ResourceLabelsWithNamespace())
- pv.Spec.Capacity = corev1.ResourceList{corev1.ResourceStorage: storage}
- pv.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}
- pv.Spec.PersistentVolumeReclaimPolicy = corev1.PersistentVolumeReclaimRetain
- pv.Spec.StorageClassName = ""
- pv.Spec.VolumeMode = nil
- pv.Spec.PersistentVolumeSource = corev1.PersistentVolumeSource{
- HostPath: &corev1.HostPathVolumeSource{
- Path: hostPath,
- Type: &hostPathType,
- },
- }
- return nil
- })
- return pvName, err
- }
- func (r *DefaultComponentReconciler) reconcilePersistentVolumeClaim(ctx context.Context, pvName, name, size string) error {
- pvc := &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName(name), Namespace: r.component.GetNamespace()}}
- storage := resource.MustParse(size)
- emptyStorageClass := ""
- _, err := controllerutil.CreateOrUpdate(ctx, r.client, pvc, func() error {
- if pvc.Labels == nil {
- pvc.Labels = map[string]string{}
- }
- maps.Copy(pvc.Labels, r.ResourceLabels())
- pvc.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}
- pvc.Spec.VolumeName = pvName
- pvc.Spec.StorageClassName = &emptyStorageClass
- if pvc.Spec.Resources.Requests == nil {
- pvc.Spec.Resources.Requests = corev1.ResourceList{}
- }
- pvc.Spec.Resources.Requests[corev1.ResourceStorage] = storage
- return nil
- })
- return err
- }
- func (r *DefaultComponentReconciler) ReconcileKey(ctx context.Context, name, key string, keygen func() (string, error)) error {
- secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName(name), Namespace: r.component.GetNamespace()}}
- _, err := controllerutil.CreateOrUpdate(ctx, r.client, secret, func() error {
- if secret.Labels == nil {
- secret.Labels = map[string]string{}
- }
- maps.Copy(secret.Labels, r.ResourceLabels())
- if secret.Data == nil {
- value, err := keygen()
- if err != nil {
- return err
- }
- secret.Data = map[string][]byte{key: []byte(value)}
- }
- return controllerutil.SetControllerReference(r.component, secret, r.scheme)
- })
- return err
- }
- func (r *DefaultComponentReconciler) ReconcileConfig(ctx context.Context, key string, config map[string]string, isSecret bool) (string, error) {
- if err := r.reconcileEnvVarConfig(ctx, config, isSecret); err != nil {
- return "", fmt.Errorf("Failed to reconcile env var config map: %w", err)
- }
- configHash, err := r.reconcileFileConfig(ctx, key, config, isSecret)
- if err != nil {
- return "", fmt.Errorf("Failed to reconcile file config map: %w", err)
- }
- return configHash, nil
- }
- func (r *DefaultComponentReconciler) reconcileEnvVarConfig(ctx context.Context, config map[string]string, isSecret bool) error {
- configData := make(map[string]string)
- for k, v := range config {
- envVarName := strings.NewReplacer("-", "_", ".", "_").Replace(k)
- configData[strings.ToUpper(envVarName)] = v
- }
- var obj client.Object
- if isSecret {
- obj = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName("env"), Namespace: r.component.GetNamespace()}}
- } else {
- obj = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName("env"), Namespace: r.component.GetNamespace()}}
- }
- _, err := controllerutil.CreateOrUpdate(ctx, r.client, obj, func() error {
- if obj.GetLabels() == nil {
- obj.SetLabels(map[string]string{})
- }
- maps.Copy(obj.GetLabels(), r.ResourceLabels())
- switch o := obj.(type) {
- case *corev1.ConfigMap:
- o.Data = configData
- case *corev1.Secret:
- o.Data = make(map[string][]byte)
- for k, v := range configData {
- o.Data[k] = []byte(v)
- }
- }
- return controllerutil.SetControllerReference(r.component, obj, r.scheme)
- })
- return err
- }
- func (r *DefaultComponentReconciler) reconcileFileConfig(ctx context.Context, key string, config map[string]string, isSecret bool) (string, error) {
- yamlConfig, err := yaml.Marshal(config)
- if err != nil {
- return "", fmt.Errorf("Failed to marshal config to yaml: %w", err)
- }
- h := sha256.Sum256(yamlConfig)
- configHash := hex.EncodeToString(h[:])
- var obj client.Object
- if isSecret {
- obj = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName("config"), Namespace: r.component.GetNamespace()}}
- } else {
- obj = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName("config"), Namespace: r.component.GetNamespace()}}
- }
- _, err = controllerutil.CreateOrUpdate(ctx, r.client, obj, func() error {
- if obj.GetLabels() == nil {
- obj.SetLabels(map[string]string{})
- }
- maps.Copy(obj.GetLabels(), r.ResourceLabels())
- if obj.GetAnnotations() == nil {
- obj.SetAnnotations(map[string]string{})
- }
- obj.GetAnnotations()["hash"] = configHash
- switch o := obj.(type) {
- case *corev1.ConfigMap:
- o.Data = map[string]string{
- key: string(yamlConfig),
- }
- case *corev1.Secret:
- o.Data = map[string][]byte{
- key: yamlConfig,
- }
- }
- return controllerutil.SetControllerReference(r.component, obj, r.scheme)
- })
- return configHash, err
- }
|