From 9a210ca4011930f3c52ff111cd78a9ea316fbccd Mon Sep 17 00:00:00 2001 From: Lanture1064 <34346740+Lanture1064@users.noreply.github.com> Date: Mon, 21 Aug 2023 16:26:12 +0800 Subject: [PATCH] feat: add CRD LLM controller Signed-off-by: Lanture1064 --- api/v1alpha1/condition.go | 2 + controllers/llm_controller.go | 110 +++++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/condition.go b/api/v1alpha1/condition.go index 2cbb1d4c7..14aef92d9 100644 --- a/api/v1alpha1/condition.go +++ b/api/v1alpha1/condition.go @@ -34,6 +34,8 @@ const ( TypeUnknown ConditionType = "Unknown" // TypeDone resources are believed to be processed TypeDone ConditionType = "Done" + // 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..3b71f2817 100644 --- a/controllers/llm_controller.go +++ b/controllers/llm_controller.go @@ -18,11 +18,19 @@ 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" + "k8s.io/apimachinery/pkg/types" 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 +55,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 +84,85 @@ 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(ctx, 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(ctx context.Context, instance *arcadiav1alpha1.LLM, logger logr.Logger) error { + logger.Info("Testing LLM availability") + + //TODO: change URL & request for different types of LLM instance + // For openai instance, we use the "GET model" api. + // For Zhipuai instance, we send a standard async request. + testURL := instance.Spec.URL + "/v1/models" + + if instance.Spec.Auth == "" { + return fmt.Errorf("auth is empty") + } + + // get auth by secret name + var auth string + secret := &corev1.Secret{} + err := r.Get(ctx, types.NamespacedName{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 +}