Browse Source

feat(skill): implement Skill controller

Thomas Zhang 2 months ago
parent
commit
4c53ea0c2e
2 changed files with 99 additions and 11 deletions
  1. 97 11
      internal/controller/skill_controller.go
  2. 2 0
      internal/reconciler/common.go

+ 97 - 11
internal/controller/skill_controller.go

@@ -18,13 +18,22 @@ package controller
 
 
 import (
 import (
 	"context"
 	"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"
 	"k8s.io/apimachinery/pkg/runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 
 
 	"github.com/LocoStack/loco-operator/api/v1alpha1"
 	"github.com/LocoStack/loco-operator/api/v1alpha1"
+	"github.com/LocoStack/loco-operator/internal/reconciler"
 )
 )
 
 
 // SkillReconciler reconciles a Skill object
 // SkillReconciler reconciles a Skill object
@@ -36,28 +45,105 @@ type SkillReconciler struct {
 // +kubebuilder:rbac:groups=locostack.com,resources=skills,verbs=get;list;watch;create;update;patch;delete
 // +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/status,verbs=get;update;patch
 // +kubebuilder:rbac:groups=locostack.com,resources=skills/finalizers,verbs=update
 // +kubebuilder:rbac:groups=locostack.com,resources=skills/finalizers,verbs=update
+// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch
 
 
-// Reconcile is part of the main kubernetes reconciliation loop which aims to
-// move the current state of the cluster closer to the desired state.
-// TODO(user): Modify the Reconcile function to compare the state specified by
-// the Skill object against the actual cluster state, and then
-// perform operations to make the cluster state reflect the state specified by
-// the user.
-//
-// For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
 func (r *SkillReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 func (r *SkillReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	_ = logf.FromContext(ctx)
+	log := logf.FromContext(ctx)
 
 
-	// TODO(user): your logic here
+	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
 	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.
 // SetupWithManager sets up the controller with the Manager.
 func (r *SkillReconciler) SetupWithManager(mgr ctrl.Manager) error {
 func (r *SkillReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&v1alpha1.Skill{}).
 		For(&v1alpha1.Skill{}).
+		Owns(&corev1.ConfigMap{}).
 		Named("skill").
 		Named("skill").
 		Complete(r)
 		Complete(r)
 }
 }

+ 2 - 0
internal/reconciler/common.go

@@ -196,6 +196,8 @@ func (r *EndpointResolver) ResolveEndpoint(ctx context.Context, stack *v1alpha1.
 		return r.resolveExternalModelEndpoint(ctx, stack, ref.Name)
 		return r.resolveExternalModelEndpoint(ctx, stack, ref.Name)
 	case "ManagedModel":
 	case "ManagedModel":
 		return r.resolveManagedModelEndpoint(ctx, stack, ref.Name)
 		return r.resolveManagedModelEndpoint(ctx, stack, ref.Name)
+	case "Skill":
+		return nil, "", nil
 	case "ExternalTool":
 	case "ExternalTool":
 		return r.resolveExternalToolEndpoint(ctx, stack, ref.Name)
 		return r.resolveExternalToolEndpoint(ctx, stack, ref.Name)
 	case "ManagedTool":
 	case "ManagedTool":