diff --git a/api/v1alpha1/condition.go b/api/v1alpha1/condition.go index a2ea5b028..557855db4 100644 --- a/api/v1alpha1/condition.go +++ b/api/v1alpha1/condition.go @@ -32,6 +32,8 @@ const ( TypeReady ConditionType = "Ready" // TypeUnknown resources are unknown to the system TypeUnknown ConditionType = "Unknown" + // TypeUnavailable resources are unavailable + TypeUnavailable ConditionType = "Unavailable" ) // A ConditionReason represents the reason a resource is in a condition. diff --git a/controllers/llm_controller.go b/controllers/llm_controller.go index 0df542135..491e61f3f 100644 --- a/controllers/llm_controller.go +++ b/controllers/llm_controller.go @@ -18,11 +18,18 @@ package controllers import ( "context" + "fmt" + "net/http" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + 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/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" arcadiav1alpha1 "github.com/kubeagi/arcadia/api/v1alpha1" ) @@ -47,10 +54,26 @@ type LLMReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile func (r *LLMReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + logger := log.FromContext(ctx) + logger.Info("Reconciling LLM resource") - // TODO(user): your logic here + // Fetch the LLM instance + instance := &arcadiav1alpha1.LLM{} + err := r.Get(ctx, req.NamespacedName, instance) + if err != nil { + if errors.IsNotFound(err) { + // LLM instance has been deleted. + return reconcile.Result{}, nil + } + return ctrl.Result{}, client.IgnoreNotFound(err) + } + err = r.CheckLLM(ctx, logger, instance) + if err != nil { + return ctrl.Result{}, err + } + + logger.Info("Instance is updated and synchronized") return ctrl.Result{}, nil } @@ -60,3 +83,80 @@ func (r *LLMReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&arcadiav1alpha1.LLM{}). Complete(r) } + +// CheckLLM updates new LLM instance. +func (r *LLMReconciler) CheckLLM(ctx context.Context, logger logr.Logger, instance *arcadiav1alpha1.LLM) error { + logger.Info("Checking LLM instance") + // Check new URL/Auth availability + err := r.TestLLMAvailability(instance, logger) + if err != nil { + // Set status to unavailable + instance.Status.SetConditions(arcadiav1alpha1.Condition{ + Type: arcadiav1alpha1.TypeUnavailable, + Status: corev1.ConditionFalse, + Reason: arcadiav1alpha1.ReasonUnavailable, + Message: err.Error(), + LastTransitionTime: metav1.Now(), + }) + } else { + // Set status to available + instance.Status.SetConditions(arcadiav1alpha1.Condition{ + Type: arcadiav1alpha1.TypeReady, + Status: corev1.ConditionTrue, + Reason: arcadiav1alpha1.ReasonAvailable, + Message: "Available", + LastTransitionTime: metav1.Now(), + LastSuccessfulTime: metav1.Now(), + }) + } + return r.Client.Status().Update(ctx, instance) +} + +// TestLLMAvailability tests LLM availability. +func (r *LLMReconciler) TestLLMAvailability(instance *arcadiav1alpha1.LLM, logger logr.Logger) error { + logger.Info("Testing LLM availability") + testURL := instance.Spec.URL + "/v1/models" + + if instance.Spec.Auth == "" { + return fmt.Errorf("auth is empty") + } + + // get auth by secret name + secret := &corev1.Secret{} + err := r.Get(context.TODO(), client.ObjectKey{Name: instance.Spec.Auth, Namespace: instance.Namespace}, secret) + if err != nil { + return err + } + + auth := "Bearer " + string(secret.Data["apiKey"]) + + err = SendTestRequest("GET", testURL, auth) + if err != nil { + return err + } + + return nil +} + +func SendTestRequest(method string, url string, auth string) error { + req, err := http.NewRequest(method, url, nil) + if err != nil { + return err + } + + req.Header.Set("Authorization", auth) + req.Header.Set("Content-Type", "application/json") + + cli := &http.Client{} + resp, err := cli.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("returns unexpected status code: %d", resp.StatusCode) + } + + return nil +}