| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- /*
- 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 controller
- import (
- "context"
- "encoding/json"
- "fmt"
- "maps"
- corev1 "k8s.io/api/core/v1"
- apierrors "k8s.io/apimachinery/pkg/api/errors"
- apimeta "k8s.io/apimachinery/pkg/api/meta"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/runtime"
- ctrl "sigs.k8s.io/controller-runtime"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
- logf "sigs.k8s.io/controller-runtime/pkg/log"
- "github.com/LocoStack/loco-operator/api/v1alpha1"
- "github.com/LocoStack/loco-operator/internal/reconciler"
- )
- // SkillReconciler reconciles a Skill object
- type SkillReconciler struct {
- client.Client
- Scheme *runtime.Scheme
- }
- // +kubebuilder:rbac:groups=locostack.com,resources=skills,verbs=get;list;watch;create;update;patch;delete
- // +kubebuilder:rbac:groups=locostack.com,resources=skills/status,verbs=get;update;patch
- // +kubebuilder:rbac:groups=locostack.com,resources=skills/finalizers,verbs=update
- // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch
- func (r *SkillReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
- log := logf.FromContext(ctx)
- sk := &v1alpha1.Skill{}
- if err := r.Get(ctx, req.NamespacedName, sk); err != nil {
- if apierrors.IsNotFound(err) {
- return ctrl.Result{}, nil
- }
- log.Error(err, "Failed to get Skill", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
- return ctrl.Result{}, err
- }
- patch := client.MergeFrom(sk.DeepCopy())
- skillAvailable, files, err := r.reconcileSkillConfigMap(ctx, sk)
- if err != nil {
- return ctrl.Result{}, err
- }
- if !skillAvailable {
- apimeta.SetStatusCondition(&sk.Status.Conditions, metav1.Condition{
- Type: "Available",
- Status: metav1.ConditionFalse,
- Reason: "SkillUnavailable",
- Message: "Skill is not available",
- ObservedGeneration: sk.Generation,
- })
- } else {
- apimeta.SetStatusCondition(&sk.Status.Conditions, metav1.Condition{
- Type: "Available",
- Status: metav1.ConditionTrue,
- Reason: "SkillAvailable",
- Message: "Skill is available",
- ObservedGeneration: sk.Generation,
- })
- }
- sk.Status.Files = files
- sk.Status.ObservedGeneration = sk.Generation
- if err := r.Status().Patch(ctx, sk, patch); err != nil {
- return ctrl.Result{}, client.IgnoreNotFound(err)
- }
- return ctrl.Result{}, nil
- }
- func (r *SkillReconciler) reconcileSkillConfigMap(ctx context.Context, sk *v1alpha1.Skill) (bool, int32, error) {
- files := int32(1)
- skillMD := fmt.Sprintf(`---
- name: %s
- description: %s.
- ---
- %s`, sk.Name, sk.Spec.Description, sk.Spec.Body)
- cmData := map[string]string{
- v1alpha1.SkillConfigMapKey("SKILL.md"): skillMD,
- }
- filePaths := map[string]string{
- v1alpha1.SkillConfigMapKey("SKILL.md"): "SKILL.md",
- }
- for _, fileEntry := range sk.Spec.Files {
- key := v1alpha1.SkillConfigMapKey(fileEntry.Path)
- cmData[key] = fileEntry.Content
- filePaths[key] = fileEntry.Path
- files++
- }
- filePathsJSON, err := json.Marshal(filePaths)
- if err != nil {
- return false, 0, fmt.Errorf("marshaling skill file paths for %s: %w", sk.Name, err)
- }
- cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: sk.Name, Namespace: sk.Namespace}}
- _, err = controllerutil.CreateOrUpdate(ctx, r.Client, cm, func() error {
- if cm.Labels == nil {
- cm.Labels = make(map[string]string)
- }
- stackName := ""
- if sk.Spec.StackRef != nil {
- stackName = sk.Spec.StackRef.Name
- }
- maps.Copy(cm.Labels, reconciler.ResourceLabels(stackName, "Skill", sk.Name, ""))
- if cm.Annotations == nil {
- cm.Annotations = make(map[string]string)
- }
- cm.Annotations[v1alpha1.SkillFilePathsAnnotation] = string(filePathsJSON)
- cm.Data = cmData
- return controllerutil.SetControllerReference(sk, cm, r.Scheme)
- })
- return err == nil, files, err
- }
- // SetupWithManager sets up the controller with the Manager.
- func (r *SkillReconciler) SetupWithManager(mgr ctrl.Manager) error {
- return ctrl.NewControllerManagedBy(mgr).
- For(&v1alpha1.Skill{}).
- Owns(&corev1.ConfigMap{}).
- Named("skill").
- Complete(r)
- }
|