From c9aff6286d68b0bbf0f54c674bf4dd8a07fa153e Mon Sep 17 00:00:00 2001 From: Ozeliurs Date: Thu, 21 Nov 2024 08:17:33 +0100 Subject: [PATCH] feat: add support for Conditions in CustomModelStatus and ModelStatus to track the observed state of the cluster --- api/v1/custommodel_types.go | 1 + api/v1/zz_generated.deepcopy.go | 9 ++- ...ama.ollama.startupnation_custommodels.yaml | 63 ++++++++++++++++ internal/controller/custommodel_controller.go | 72 ++++++++++++++----- internal/controller/model_controller.go | 30 ++++++++ 5 files changed, 157 insertions(+), 18 deletions(-) diff --git a/api/v1/custommodel_types.go b/api/v1/custommodel_types.go index b0cbde3..3b9ea9b 100644 --- a/api/v1/custommodel_types.go +++ b/api/v1/custommodel_types.go @@ -37,6 +37,7 @@ type CustomModelSpec struct { type CustomModelStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file + Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 1bb57c8..700f0f5 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -31,7 +31,7 @@ func (in *CustomModel) DeepCopyInto(out *CustomModel) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomModel. @@ -102,6 +102,13 @@ func (in *CustomModelSpec) DeepCopy() *CustomModelSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CustomModelStatus) DeepCopyInto(out *CustomModelStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomModelStatus. diff --git a/config/crd/bases/ollama.ollama.startupnation_custommodels.yaml b/config/crd/bases/ollama.ollama.startupnation_custommodels.yaml index 7aee2c4..7ea8ca1 100644 --- a/config/crd/bases/ollama.ollama.startupnation_custommodels.yaml +++ b/config/crd/bases/ollama.ollama.startupnation_custommodels.yaml @@ -43,12 +43,75 @@ spec: type: string modelName: type: string + ollamaUrl: + type: string required: - modelFile - modelName + - ollamaUrl type: object status: description: CustomModelStatus defines the observed state of CustomModel + properties: + conditions: + description: |- + INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + Important: Run "make" to regenerate code after modifying this file + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array type: object type: object served: true diff --git a/internal/controller/custommodel_controller.go b/internal/controller/custommodel_controller.go index d56dfd2..9d917c0 100644 --- a/internal/controller/custommodel_controller.go +++ b/internal/controller/custommodel_controller.go @@ -20,6 +20,7 @@ import ( "context" "github.com/StartUpNationLabs/simple-ollama-operator/internal/ollama_client" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/reconcile" "k8s.io/apimachinery/pkg/runtime" @@ -52,15 +53,15 @@ type CustomModelReconciler struct { func (r *CustomModelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) - // Fetch the Model instance - CustomModel := &ollamav1.CustomModel{} - err := r.Get(ctx, req.NamespacedName, CustomModel) + // Fetch the CustomModel instance + customModel := &ollamav1.CustomModel{} + err := r.Get(ctx, req.NamespacedName, customModel) if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // create a new Ollama Client - ollamaUrl := CustomModel.Spec.OllamaUrl + ollamaUrl := customModel.Spec.OllamaUrl ollamaClient, err := ollama_client.NewClientWithResponses(ollamaUrl) if err != nil { logger.Error(err, "unable to create Ollama Client") @@ -68,21 +69,21 @@ func (r *CustomModelReconciler) Reconcile(ctx context.Context, req ctrl.Request) } logger.Info("Ollama Client created") // If the CustomModel is being deleted, delete it from the Ollama Client - CustomModelFinalizer := "CustomModel.finalizer.ollama.ollama.startupnation" + customModelFinalizer := "custommodel.finalizer.ollama.ollama.startupnation" - modelName := unifyModelName(CustomModel.Spec.ModelName) - if CustomModel.ObjectMeta.DeletionTimestamp.IsZero() { + modelName := unifyModelName(customModel.Spec.ModelName) + if customModel.ObjectMeta.DeletionTimestamp.IsZero() { // The object is not being deleted, so if it does not have our finalizer, // then lets add the finalizer and update the object. - if !containsString(CustomModel.ObjectMeta.Finalizers, CustomModelFinalizer) { - CustomModel.ObjectMeta.Finalizers = append(CustomModel.ObjectMeta.Finalizers, CustomModelFinalizer) - if err := r.Update(context.Background(), CustomModel); err != nil { + if !containsString(customModel.ObjectMeta.Finalizers, customModelFinalizer) { + customModel.ObjectMeta.Finalizers = append(customModel.ObjectMeta.Finalizers, customModelFinalizer) + if err := r.Update(context.Background(), customModel); err != nil { return reconcile.Result{}, err } } } else { // The object is being deleted - if containsString(CustomModel.ObjectMeta.Finalizers, CustomModelFinalizer) { + if containsString(customModel.ObjectMeta.Finalizers, customModelFinalizer) { // our finalizer is present, so lets handle our external dependency // first, we delete the external dependency logger.Info("Deleting CustomModel", "CustomModel Name", modelName, "Ollama URL", ollamaUrl) @@ -94,8 +95,8 @@ func (r *CustomModelReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // remove our finalizer from the list and update it. - CustomModel.ObjectMeta.Finalizers = removeString(CustomModel.ObjectMeta.Finalizers, CustomModelFinalizer) - if err := r.Update(context.Background(), CustomModel); err != nil { + customModel.ObjectMeta.Finalizers = removeString(customModel.ObjectMeta.Finalizers, customModelFinalizer) + if err := r.Update(context.Background(), customModel); err != nil { return reconcile.Result{}, err } } @@ -105,6 +106,11 @@ func (r *CustomModelReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // if the CustomModel is not being deleted, start reconciliation + // Update status to indicate reconciliation has started + if err := r.updateStatus(ctx, customModel, "ReconciliationStarted", metav1.ConditionTrue, "Reconciling", "Reconciliation process has started"); err != nil { + return ctrl.Result{}, err + } + // get the CustomModel from the Ollama Client logger.Info("Checking if CustomModel exists", "CustomModel Name", modelName, "Ollama URL", ollamaUrl) res, err := ollamaClient.PostApiShowWithResponse(ctx, ollama_client.PostApiShowJSONRequestBody{ @@ -115,8 +121,12 @@ func (r *CustomModelReconciler) Reconcile(ctx context.Context, req ctrl.Request) logger.Info("CustomModel exists", "CustomModel Name", modelName, "Ollama URL", ollamaUrl) if res.JSON200 != nil { logger.Info("Checking if the ModelFile is the same", "CustomModel Name", modelName, "Ollama URL", ollamaUrl) - if *res.JSON200.Parameters == CustomModel.Spec.ModelFile { + if *res.JSON200.Parameters == customModel.Spec.ModelFile { logger.Info("ModelFile is the same", "CustomModel Name", modelName, "Ollama URL", ollamaUrl) + // Update status to indicate model exists + if err := r.updateStatus(ctx, customModel, "CustomModelExists", metav1.ConditionTrue, "ModelFound", "CustomModel exists in Ollama"); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, nil } logger.Info("ModelFile is not the same", "CustomModel Name", modelName, "Ollama URL", ollamaUrl) @@ -133,33 +143,61 @@ func (r *CustomModelReconciler) Reconcile(ctx context.Context, req ctrl.Request) stream := false _, err = ollamaClient.PostApiCreate(ctx, ollama_client.PostApiCreateJSONRequestBody{ Name: &modelName, - Modelfile: &CustomModel.Spec.ModelFile, + Modelfile: &customModel.Spec.ModelFile, Stream: &stream, }) if err != nil { logger.Error(err, "unable to create CustomModel") + // Update status to indicate model creation failed + if err := r.updateStatus(ctx, customModel, "CustomModelCreationFailed", metav1.ConditionTrue, "CreationFailed", "Failed to create CustomModel in Ollama"); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } } + // Update status to indicate model creation succeeded + if err := r.updateStatus(ctx, customModel, "CustomModelCreated", metav1.ConditionTrue, "CreationSucceeded", "CustomModel created successfully in Ollama"); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } // if the CustomModel does not exist, create it - logger.Info("CustomModel does not exist, creating CustomModel", "CustomModel Name", modelName, "Ollama URL", ollamaUrl, "ModelFile", CustomModel.Spec.ModelFile) + logger.Info("CustomModel does not exist, creating CustomModel", "CustomModel Name", modelName, "Ollama URL", ollamaUrl, "ModelFile", customModel.Spec.ModelFile) stream := false _, err = ollamaClient.PostApiCreate(ctx, ollama_client.PostApiCreateJSONRequestBody{ Name: &modelName, - Modelfile: &CustomModel.Spec.ModelFile, + Modelfile: &customModel.Spec.ModelFile, Stream: &stream, }) if err != nil || res.StatusCode() != 200 { logger.Error(err, "unable to create CustomModel") + // Update status to indicate model creation failed + if err := r.updateStatus(ctx, customModel, "CustomModelCreationFailed", metav1.ConditionTrue, "CreationFailed", "Failed to create CustomModel in Ollama"); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } logger.Info("CustomModel created", "CustomModel Name", modelName, "Ollama URL", ollamaUrl) + // Update status to indicate model creation succeeded + if err := r.updateStatus(ctx, customModel, "CustomModelCreated", metav1.ConditionTrue, "CreationSucceeded", "CustomModel created successfully in Ollama"); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, nil } +// updateStatus is a helper function to update the status conditions of the CustomModel +func (r *CustomModelReconciler) updateStatus(ctx context.Context, customModel *ollamav1.CustomModel, conditionType string, status metav1.ConditionStatus, reason string, message string) error { + customModel.Status.Conditions = append(customModel.Status.Conditions, metav1.Condition{ + Type: conditionType, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + }) + return r.Status().Update(ctx, customModel) +} + // SetupWithManager sets up the controller with the Manager. func (r *CustomModelReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). diff --git a/internal/controller/model_controller.go b/internal/controller/model_controller.go index 5496263..4a7d45c 100644 --- a/internal/controller/model_controller.go +++ b/internal/controller/model_controller.go @@ -20,6 +20,7 @@ import ( "context" "github.com/StartUpNationLabs/simple-ollama-operator/internal/ollama_client" + 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" @@ -106,6 +107,11 @@ func (r *ModelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl } // if the Model is not being deleted, start reconciliation + // Update status to indicate reconciliation has started + if err := r.updateStatus(ctx, model, "ReconciliationStarted", metav1.ConditionTrue, "Reconciling", "Reconciliation process has started"); err != nil { + return ctrl.Result{}, err + } + // get the model from the Ollama Client logger.Info("Checking if Model exists", "Model Name", modelName, "Ollama URL", ollamaUrl) res, err := ollamaClient.PostApiShowWithResponse(ctx, ollama_client.PostApiShowJSONRequestBody{ @@ -116,6 +122,10 @@ func (r *ModelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl logger.Info("Model exists", "Model Name", modelName, "Ollama URL", ollamaUrl) if res.JSON200 != nil { logger.Info("Model exists", "Model Params", res.JSON200.Parameters, "Ollama URL", ollamaUrl) + // Update status to indicate model exists + if err := r.updateStatus(ctx, model, "ModelExists", metav1.ConditionTrue, "ModelFound", "Model exists in Ollama"); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, nil } return ctrl.Result{}, err @@ -129,13 +139,33 @@ func (r *ModelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl }) if err != nil { logger.Error(err, "unable to create Model") + // Update status to indicate model creation failed + if err := r.updateStatus(ctx, model, "ModelCreationFailed", metav1.ConditionTrue, "CreationFailed", "Failed to create model in Ollama"); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, err } logger.Info("Model created", "Model Name", modelName, "Ollama URL", ollamaUrl) + // Update status to indicate model creation succeeded + if err := r.updateStatus(ctx, model, "ModelCreated", metav1.ConditionTrue, "CreationSucceeded", "Model created successfully in Ollama"); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, nil } +// updateStatus is a helper function to update the status conditions of the Model +func (r *ModelReconciler) updateStatus(ctx context.Context, model *ollamav1.Model, conditionType string, status metav1.ConditionStatus, reason string, message string) error { + model.Status.Conditions = append(model.Status.Conditions, metav1.Condition{ + Type: conditionType, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + }) + return r.Status().Update(ctx, model) +} + // SetupWithManager sets up the controller with the Manager. func (r *ModelReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr).