skill_controller.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  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 controller
  14. import (
  15. "context"
  16. "encoding/json"
  17. "fmt"
  18. "maps"
  19. corev1 "k8s.io/api/core/v1"
  20. apierrors "k8s.io/apimachinery/pkg/api/errors"
  21. apimeta "k8s.io/apimachinery/pkg/api/meta"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/runtime"
  24. ctrl "sigs.k8s.io/controller-runtime"
  25. "sigs.k8s.io/controller-runtime/pkg/client"
  26. "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
  27. logf "sigs.k8s.io/controller-runtime/pkg/log"
  28. "github.com/LocoStack/loco-operator/api/v1alpha1"
  29. "github.com/LocoStack/loco-operator/internal/reconciler"
  30. )
  31. // SkillReconciler reconciles a Skill object
  32. type SkillReconciler struct {
  33. client.Client
  34. Scheme *runtime.Scheme
  35. }
  36. // +kubebuilder:rbac:groups=locostack.com,resources=skills,verbs=get;list;watch;create;update;patch;delete
  37. // +kubebuilder:rbac:groups=locostack.com,resources=skills/status,verbs=get;update;patch
  38. // +kubebuilder:rbac:groups=locostack.com,resources=skills/finalizers,verbs=update
  39. // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch
  40. func (r *SkillReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  41. log := logf.FromContext(ctx)
  42. sk := &v1alpha1.Skill{}
  43. if err := r.Get(ctx, req.NamespacedName, sk); err != nil {
  44. if apierrors.IsNotFound(err) {
  45. return ctrl.Result{}, nil
  46. }
  47. log.Error(err, "Failed to get Skill", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
  48. return ctrl.Result{}, err
  49. }
  50. patch := client.MergeFrom(sk.DeepCopy())
  51. skillAvailable, files, err := r.reconcileSkillConfigMap(ctx, sk)
  52. if err != nil {
  53. return ctrl.Result{}, err
  54. }
  55. if !skillAvailable {
  56. apimeta.SetStatusCondition(&sk.Status.Conditions, metav1.Condition{
  57. Type: "Available",
  58. Status: metav1.ConditionFalse,
  59. Reason: "SkillUnavailable",
  60. Message: "Skill is not available",
  61. ObservedGeneration: sk.Generation,
  62. })
  63. } else {
  64. apimeta.SetStatusCondition(&sk.Status.Conditions, metav1.Condition{
  65. Type: "Available",
  66. Status: metav1.ConditionTrue,
  67. Reason: "SkillAvailable",
  68. Message: "Skill is available",
  69. ObservedGeneration: sk.Generation,
  70. })
  71. }
  72. sk.Status.Files = files
  73. sk.Status.ObservedGeneration = sk.Generation
  74. if err := r.Status().Patch(ctx, sk, patch); err != nil {
  75. return ctrl.Result{}, client.IgnoreNotFound(err)
  76. }
  77. return ctrl.Result{}, nil
  78. }
  79. func (r *SkillReconciler) reconcileSkillConfigMap(ctx context.Context, sk *v1alpha1.Skill) (bool, int32, error) {
  80. files := int32(1)
  81. skillMD := fmt.Sprintf(`---
  82. name: %s
  83. description: %s.
  84. ---
  85. %s`, sk.Name, sk.Spec.Description, sk.Spec.Body)
  86. cmData := map[string]string{
  87. v1alpha1.SkillConfigMapKey("SKILL.md"): skillMD,
  88. }
  89. filePaths := map[string]string{
  90. v1alpha1.SkillConfigMapKey("SKILL.md"): "SKILL.md",
  91. }
  92. for _, fileEntry := range sk.Spec.Files {
  93. key := v1alpha1.SkillConfigMapKey(fileEntry.Path)
  94. cmData[key] = fileEntry.Content
  95. filePaths[key] = fileEntry.Path
  96. files++
  97. }
  98. filePathsJSON, err := json.Marshal(filePaths)
  99. if err != nil {
  100. return false, 0, fmt.Errorf("marshaling skill file paths for %s: %w", sk.Name, err)
  101. }
  102. cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: sk.Name, Namespace: sk.Namespace}}
  103. _, err = controllerutil.CreateOrUpdate(ctx, r.Client, cm, func() error {
  104. if cm.Labels == nil {
  105. cm.Labels = make(map[string]string)
  106. }
  107. stackName := ""
  108. if sk.Spec.StackRef != nil {
  109. stackName = sk.Spec.StackRef.Name
  110. }
  111. maps.Copy(cm.Labels, reconciler.ResourceLabels(stackName, "Skill", sk.Name, ""))
  112. if cm.Annotations == nil {
  113. cm.Annotations = make(map[string]string)
  114. }
  115. cm.Annotations[v1alpha1.SkillFilePathsAnnotation] = string(filePathsJSON)
  116. cm.Data = cmData
  117. return controllerutil.SetControllerReference(sk, cm, r.Scheme)
  118. })
  119. return err == nil, files, err
  120. }
  121. // SetupWithManager sets up the controller with the Manager.
  122. func (r *SkillReconciler) SetupWithManager(mgr ctrl.Manager) error {
  123. return ctrl.NewControllerManagedBy(mgr).
  124. For(&v1alpha1.Skill{}).
  125. Owns(&corev1.ConfigMap{}).
  126. Named("skill").
  127. Complete(r)
  128. }