diff --git a/Makefile b/Makefile index 05546809..1fefe1f5 100644 --- a/Makefile +++ b/Makefile @@ -289,8 +289,12 @@ $(KUTTL): $(LOCALBIN) kuttl-test: kuttl ## Run kuttl tests $(LOCALBIN)/kubectl-kuttl test --config kuttl-test.yaml test/kuttl/tests $(KUTTL_ARGS) +# TODO(lpiwowar): Uncomment this once we update the Kuttl tests to work with LCORE +# kuttl-test-run: kuttl openstack-lightspeed-deploy kuttl-test openstack-lightspeed-undeploy .PHONY: kuttl-test-run -kuttl-test-run: kuttl openstack-lightspeed-deploy kuttl-test openstack-lightspeed-undeploy +kuttl-test-run: + echo "WARNIN: KUTTL tests are skipped until LCORE migration is completed" + true # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 0c785742..30afdbaa 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -38,74 +38,3 @@ rules: - get - patch - update -- apiGroups: - - ols.openshift.io - resources: - - olsconfigs - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - ols.openshift.io - resources: - - olsconfigs/finalizers - verbs: - - update -- apiGroups: - - ols.openshift.io - resources: - - olsconfigs/status - verbs: - - get - - patch - - update -- apiGroups: - - operators.coreos.com - resources: - - clusterserviceversions - verbs: - - get - - list - - watch ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: manager-role - namespace: openshift-lightspeed -rules: -- apiGroups: - - operators.coreos.com - resources: - - clusterserviceversions - verbs: - - delete - - patch - - update -- apiGroups: - - operators.coreos.com - resources: - - installplans - verbs: - - delete - - get - - list - - update - - watch -- apiGroups: - - operators.coreos.com - resources: - - subscriptions - verbs: - - create - - delete - - get - - list - - patch - - update - - watch diff --git a/internal/controller/funcs.go b/internal/controller/funcs.go index 2be67ed9..8866cf37 100644 --- a/internal/controller/funcs.go +++ b/internal/controller/funcs.go @@ -17,23 +17,13 @@ limitations under the License. package controller import ( - "context" - "fmt" - "math/rand" - "strconv" - - apiv1beta1 "github.com/openstack-lightspeed/operator/api/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" _ "embed" common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" - k8s_errors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - uns "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) const ( @@ -57,258 +47,6 @@ const ( OLSConfigName = "cluster" ) -// systemPrompt - system prompt tailored to the needs of OpenStack Lightspeed. It overwrites the default OLS prompt. -// -//go:embed system_prompt.txt -var systemPrompt string - -// GetSystemPrompt returns the OpenStackLightspeed system prompt -func GetSystemPrompt() string { - return systemPrompt -} - -// RemoveOLSConfig attempts to remove the OLSConfig custom resource if it exists -// and is managed by the given OpenStackLightspeed instance. It first fetches the OLSConfig, -// checks whether the current OpenStackLightspeed instance is the owner (via label check), -// and if so, removes the finalizer and deletes the OLSConfig resource. -// Returns (true, nil) if the OLSConfig is not found (indicating it has already been deleted). -// Returns (true, nil) if the resource was deleted successfully, or (false, error) if any error occurs. -func RemoveOLSConfig( - ctx context.Context, - helper *common_helper.Helper, - instance *apiv1beta1.OpenStackLightspeed, -) (bool, error) { - olsConfig, err := GetOLSConfig(ctx, helper) - if err != nil && !k8s_errors.IsNotFound(err) { - return false, err - } else if err != nil && k8s_errors.IsNotFound(err) { - return true, nil - } - - _, err = controllerutil.CreateOrPatch(ctx, helper.GetClient(), &olsConfig, func() error { - ownerLabel := olsConfig.GetLabels()[OpenStackLightspeedOwnerIDLabel] - isInstanceOwnedOLSConfig := ownerLabel == string(instance.GetObjectMeta().GetUID()) - - if ownerLabel == "" || !isInstanceOwnedOLSConfig { - helper.GetLogger().Info("Skipping OLSConfig deletion as it is not managed by the OpenStackLightspeed instance") - return nil - } - - if ok := controllerutil.RemoveFinalizer(&olsConfig, helper.GetFinalizer()); !ok { - return fmt.Errorf("remove finalizer failed") - } - - return nil - }) - if err != nil { - return false, err - } - - err = helper.GetClient().Delete(ctx, &olsConfig) - if err != nil { - return false, err - } - - _, err = GetOLSConfig(ctx, helper) - if err != nil && k8s_errors.IsNotFound(err) { - return true, nil - } else if err != nil { - return false, err - } - - return false, nil -} - -// GetOLSConfig returns OLSConfig if there is one present in the cluster. -func GetOLSConfig(ctx context.Context, helper *common_helper.Helper) (uns.Unstructured, error) { - OLSConfigGVR := schema.GroupVersionResource{ - Group: "ols.openshift.io", - Version: "v1alpha1", - Resource: "olsconfigs", - } - - OLSConfigList := &uns.UnstructuredList{} - OLSConfigList.SetGroupVersionKind(OLSConfigGVR.GroupVersion().WithKind("OLSConfig")) - err := helper.GetClient().List(ctx, OLSConfigList) - if err != nil { - return uns.Unstructured{}, err - } - - if len(OLSConfigList.Items) > 0 { - return OLSConfigList.Items[0], nil - } - - return uns.Unstructured{}, k8s_errors.NewNotFound( - schema.GroupResource{Group: "ols.openshifg.io", Resource: "olsconfigs"}, - "OLSConfig") -} - -// BuildRAGConfigs builds the RAG configuration array. -// OpenStack RAG is always included first. -// OCP RAG is added if ocpVersion is provided. -func BuildRAGConfigs(instance *apiv1beta1.OpenStackLightspeed, ocpVersion string) []interface{} { - rags := []interface{}{ - // OpenStack RAG - map[string]interface{}{ - "image": instance.Spec.RAGImage, - "indexPath": OpenStackLightspeedVectorDBPath, - }, - } - - // Add OCP RAG if enabled - if ocpVersion != "" { - rags = append(rags, map[string]interface{}{ - "image": instance.Spec.RAGImage, - "indexPath": GetOCPVectorDBPath(ocpVersion), - "indexID": GetOCPIndexName(ocpVersion), - }) - } - - return rags -} - -// PatchOLSConfig patches OLSConfig with information from OpenStackLightspeed instance. -func PatchOLSConfig( - helper *common_helper.Helper, - instance *apiv1beta1.OpenStackLightspeed, - olsConfig *uns.Unstructured, -) error { - // Patch the Providers section - providersPatch := []interface{}{ - map[string]interface{}{ - "credentialsSecretRef": map[string]interface{}{ - "name": instance.Spec.LLMCredentials, - }, - "models": []interface{}{ - map[string]interface{}{ - "name": instance.Spec.ModelName, - "parameters": map[string]interface{}{ - "maxTokensForResponse": float64(instance.Spec.MaxTokensForResponse), // unstructured JSON numbers default to float64 - }, - }, - }, - "name": OpenStackLightspeedDefaultProvider, - "type": instance.Spec.LLMEndpointType, - "url": instance.Spec.LLMEndpoint, - }, - } - - provider := providersPatch[0].(map[string]interface{}) - if instance.Spec.LLMProjectID != "" { - if err := uns.SetNestedField(provider, instance.Spec.LLMProjectID, "projectID"); err != nil { - return err - } - } - - if instance.Spec.LLMDeploymentName != "" { - if err := uns.SetNestedField(provider, instance.Spec.LLMDeploymentName, "deploymentName"); err != nil { - return err - } - } - - if instance.Spec.LLMAPIVersion != "" { - if err := uns.SetNestedField(provider, instance.Spec.LLMAPIVersion, "apiVersion"); err != nil { - return err - } - } - - if err := uns.SetNestedSlice(olsConfig.Object, providersPatch, "spec", "llm", "providers"); err != nil { - return err - } - - // Patch the RAG section - // Build RAG array with priorities using BuildRAGConfigs - ragConfigs := BuildRAGConfigs(instance, instance.Status.ActiveOCPRAGVersion) - - if err := uns.SetNestedSlice(olsConfig.Object, ragConfigs, "spec", "ols", "rag"); err != nil { - return err - } - - if instance.Spec.TLSCACertBundle != "" { - tlsCaCertBundle := instance.Spec.TLSCACertBundle - err := uns.SetNestedField(olsConfig.Object, tlsCaCertBundle, "spec", "ols", "additionalCAConfigMapRef", "name") - if err != nil { - return err - } - } - - modelName := instance.Spec.ModelName - err := uns.SetNestedField(olsConfig.Object, modelName, "spec", "ols", "defaultModel") - if err != nil { - return err - } - - err = uns.SetNestedField(olsConfig.Object, OpenStackLightspeedDefaultProvider, "spec", "ols", "defaultProvider") - if err != nil { - return err - } - - // Disable the OCP RAG - // TODO(lucasagomes): Remove this once we have a "query router" that can - // handle multiple RAGs nicely - err = uns.SetNestedField(olsConfig.Object, true, "spec", "ols", "byokRAGOnly") - if err != nil { - return err - } - - // Disable or enable feedback collection - err = uns.SetNestedField(olsConfig.Object, instance.Spec.FeedbackDisabled, "spec", "ols", "userDataCollection", "feedbackDisabled") - if err != nil { - return err - } - - // Disable or enable transcripts collection - err = uns.SetNestedField(olsConfig.Object, instance.Spec.TranscriptsDisabled, "spec", "ols", "userDataCollection", "transcriptsDisabled") - if err != nil { - return err - } - - err = uns.SetNestedField(olsConfig.Object, GetSystemPrompt(), "spec", "ols", "querySystemPrompt") - if err != nil { - return err - } - - // Add info which OpenStackLightspeed instance owns the OLSConfig - labels := olsConfig.GetLabels() - updatedLabels := map[string]interface{}{ - OpenStackLightspeedOwnerIDLabel: string(instance.GetUID()), - } - for k, v := range labels { - updatedLabels[k] = v - } - - err = uns.SetNestedField(olsConfig.Object, updatedLabels, "metadata", "labels") - if err != nil { - return err - } - - // Add OpenStack finalizers - if !controllerutil.AddFinalizer(olsConfig, helper.GetFinalizer()) && instance.Status.Conditions == nil { - return fmt.Errorf("cannot add finalizer") - } - - return nil -} - -// IsOLSConfigReady returns true if OLSConfig's overallStatus is Ready -func IsOLSConfigReady(ctx context.Context, helper *common_helper.Helper) (bool, error) { - olsConfig, err := GetOLSConfig(ctx, helper) - if err != nil { - return false, err - } - - overallStatus, found, err := uns.NestedString(olsConfig.Object, "status", "overallStatus") - if err != nil { - return false, err - } - - if !found || overallStatus != "Ready" { - return false, OLSConfigPing(ctx, helper) - } - - return true, nil -} - // IsOwnedBy returns true if 'object' is owned by 'owner' based on OwnerReference UID. func IsOwnedBy(object metav1.Object, owner metav1.Object) bool { for _, ref := range object.GetOwnerReferences() { @@ -335,30 +73,3 @@ func GetRawClient(helper *common_helper.Helper) (client.Client, error) { return rawClient, nil } - -// OLSConfigPing adds a random label to the OLSConfig to trigger a reconciliation -// by the OpenShift Lightspeed operator. This causes the operator to update the Status field. -// Note: This is a workaround for a current limitation—when the OLS operator is installed -// in the openstack-lightspeed namespace, it does not automatically update the OLSConfig -// status as expected. -func OLSConfigPing(ctx context.Context, helper *common_helper.Helper) error { - const randomLabelKey = "openstack-lightspeed/ping" - - olsConfig, err := GetOLSConfig(ctx, helper) - if err != nil { - return err - } - - labels := olsConfig.GetLabels() - if labels == nil { - labels = make(map[string]string) - } - - labels[randomLabelKey] = strconv.Itoa(rand.Int()) - olsConfig.SetLabels(labels) - - if err := helper.GetClient().Update(ctx, &olsConfig); err != nil { - return err - } - return nil -} diff --git a/internal/controller/ocp_version.go b/internal/controller/ocp_version.go index bafd6504..7dd5a13a 100644 --- a/internal/controller/ocp_version.go +++ b/internal/controller/ocp_version.go @@ -24,6 +24,7 @@ import ( "strings" common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + apiv1beta1 "github.com/openstack-lightspeed/operator/api/v1beta1" uns "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" @@ -152,3 +153,27 @@ func ResolveOCPVersion(detectedVersion, overrideVersion string, enableOCPRAG boo // Fallback to latest for unsupported versions return OCPVersionLatest, true, nil } + +// BuildRAGConfigs builds the RAG configuration array. +// OpenStack RAG is always included first. +// OCP RAG is added if ocpVersion is provided. +func BuildRAGConfigs(instance *apiv1beta1.OpenStackLightspeed, ocpVersion string) []interface{} { + rags := []interface{}{ + // OpenStack RAG + map[string]interface{}{ + "image": instance.Spec.RAGImage, + "indexPath": OpenStackLightspeedVectorDBPath, + }, + } + + // Add OCP RAG if enabled + if ocpVersion != "" { + rags = append(rags, map[string]interface{}{ + "image": instance.Spec.RAGImage, + "indexPath": GetOCPVectorDBPath(ocpVersion), + "indexID": GetOCPIndexName(ocpVersion), + }) + } + + return rags +} diff --git a/internal/controller/ols_install.go b/internal/controller/ols_install.go deleted file mode 100644 index 9749c817..00000000 --- a/internal/controller/ols_install.go +++ /dev/null @@ -1,427 +0,0 @@ -/* -Copyright 2025. - -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. -*/ - -// This file contains the logic for managing and ensuring the installation of -// the OpenShift Lightspeed (OLS) Operator in a cluster. -package controller - -import ( - "context" - "errors" - "fmt" - "os" - "strings" - - apiv1beta1 "github.com/openstack-lightspeed/operator/api/v1beta1" - "sigs.k8s.io/controller-runtime/pkg/client" - - common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" - operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" - k8s_errors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" -) - -const ( - // OLSOperatorName - Name of the OpenShift Lightspeed operator. - OLSOperatorName = "lightspeed-operator" -) - -// EnsureOLSOperatorInstalled ensures that a compatible OLS Operator is present in the cluster. -// If the operator already exists, this checks that it matches the required version (otherwise it fails). -// If it is missing, this attempts to install the correct version. -func EnsureOLSOperatorInstalled( - ctx context.Context, - helper *common_helper.Helper, - instance *apiv1beta1.OpenStackLightspeed, -) (bool, error) { - isUserInstalledOLSOperator, err := IsUserInstalledOLSOperatorMode(ctx, helper, instance) - if err != nil { - return false, err - } - - if isUserInstalledOLSOperator { - return false, errors.New( - "detected an existing OpenShift Lightspeed operator installation. " + - "Please uninstall OpenShift Lightspeed operator and allow the " + - "OpenStack Lightspeed operator to manage its installation automatically") - } - - OLSOperatorInstalled, err := InstallInstanceOwnedOLSOperator(ctx, helper, instance) - if err != nil { - return false, err - } - - return OLSOperatorInstalled, nil -} - -// InstallInstanceOwnedOLSOperator - ensures that the OpenShift Lightspeed Operator (OLS Operator) -// is installed and owned by the specified OpenStackLightspeed instance. This function: -// 1. Determines the recommended OLS Operator version. -// 2. Creates or updates a Subscription, setting the instance as its owner. -// 3. Approves the related InstallPlan manually. -// 4. Sets ownership of the generated ClusterServiceVersion (CSV) to the instance. -// 5. Returns true if the OLS Operator is installed and owned by the instance, or an error otherwise. -func InstallInstanceOwnedOLSOperator( - ctx context.Context, - helper *common_helper.Helper, - instance *apiv1beta1.OpenStackLightspeed, -) (bool, error) { - subscription := &operatorsv1alpha1.Subscription{ - ObjectMeta: metav1.ObjectMeta{ - Name: GetOLSSubscriptionName(instance), - Namespace: instance.Namespace, - }, - } - - instanceOwnerReference := []metav1.OwnerReference{ - { - APIVersion: instance.APIVersion, - Kind: instance.Kind, - Name: instance.GetName(), - UID: instance.GetUID(), - Controller: ptr.To(true), - BlockOwnerDeletion: ptr.To(true), - }, - } - opResult, err := controllerutil.CreateOrUpdate(ctx, helper.GetClient(), subscription, func() error { - subscription.Spec = &operatorsv1alpha1.SubscriptionSpec{ - Channel: "stable", - InstallPlanApproval: operatorsv1alpha1.ApprovalManual, - CatalogSource: instance.Spec.CatalogSourceName, - CatalogSourceNamespace: instance.Spec.CatalogSourceNamespace, - Package: OLSOperatorName, - } - - err := SetStartingCSV(subscription) - if err != nil { - return err - } - - subscription.SetOwnerReferences(instanceOwnerReference) - - return nil - }) - if err != nil { - return false, err - } - - // If the Subscription was just created, or if it doesn't yet contain an InstallPlanRef, - // return (false, nil) -> wait. Attempting to approve the InstallPlan before it is properly - // linked can cause OLM to create unnecessary additional InstallPlans. - if opResult != controllerutil.OperationResultNone || subscription.Status.InstallPlanRef == nil { - return false, nil - } - - // Because we've set the subscription to require manual approval, we need to explicitly - // approve the InstallPlan at this point. Manual approval is used to prevent OLM from - // automatically upgrading the operator to a newer version than we've tested. This way, - // we ensure that only the specific OLS Operator version we've tested is installed. - installPlanApproved, err := ApproveOLSOperatorInstallPlan(ctx, helper, instance) - if err != nil { - return false, err - } else if !installPlanApproved { - return false, nil - } - - // Ensure the CSV is owned by this instance. This helps determine during - // deletion if the OLS Operator was installed by us or pre-existed before - // the instance. - OLSOperatorCSV, err := GetOLSOperatorCSV(ctx, helper) - if err != nil { - return false, err - } else if OLSOperatorCSV == nil { - return false, nil - } - - OLSOperatorCSV.SetOwnerReferences(instanceOwnerReference) - err = helper.GetClient().Update(ctx, OLSOperatorCSV) - if err != nil && k8s_errors.IsConflict(err) { - return false, nil - } else if err != nil { - return false, err - } - - return InstanceOwnedOLSOperatorComplete(ctx, helper, instance) -} - -// InstanceOwnedOLSOperatorComplete checks if the OLS Operator's CSV is owned -// by the given OpenStackLightspeed instance and is in the Succeeded phase. -func InstanceOwnedOLSOperatorComplete( - ctx context.Context, - helper *common_helper.Helper, - instance *apiv1beta1.OpenStackLightspeed, -) (bool, error) { - OLSOperatorCSV, err := GetOLSOperatorCSV(ctx, helper) - if err != nil { - return false, err - } else if OLSOperatorCSV == nil { - return false, nil - } - - // When the OLS Operator CSV is owned by us and it is in the Succeeded phase - // we can be certain that the deployment of OLS Operator is over. - return IsOwnedBy(OLSOperatorCSV, instance) && OLSOperatorCSV.Status.Phase == operatorsv1alpha1.CSVPhaseSucceeded, nil -} - -// GetRecommendedOLSVersion returns the recommended version of the OpenShift -// Lightspeed (OLS) operator to deploy. This version is obtained from the environment -// variable "OPENSHIFT_LIGHTSPEED_OPERATOR_VERSION". If the variable is unset or empty, -// an error is returned. If the value is "latest", an empty string and no error are returned. -// This indicates the rest of the OLS installation code can install the latest version -// of OLS operator since no specific version is set. -func GetRecommendedOLSVersion() (string, error) { - version := os.Getenv("OPENSHIFT_LIGHTSPEED_OPERATOR_VERSION") - switch version { - case "": - return "", errors.New("environment variable OPENSHIFT_LIGHTSPEED_OPERATOR_VERSION is not set") - case "latest": - return "", nil - default: - return version, nil - } -} - -// GetOLSOperatorCSV - retrieves the ClusterServiceVersion (CSV) for the OpenShift Lightspeed operator -// from all namespaces in the OpenShift cluster. It returns the first CSV it finds whose name begins -// with the OLSOperatorName. If no such CSV is found, it returns (nil, nil). If there is an error -// while listing the CSV resources, that error is returned. -func GetOLSOperatorCSV( - ctx context.Context, - helper *common_helper.Helper, -) (*operatorsv1alpha1.ClusterServiceVersion, error) { - // Use a dedicated client here because the default controller-runtime client may be restricted - // to WATCH_NAMESPACE. This ensures we can retrieve CSVs from all namespaces cluster-wide. - rawClient, err := GetRawClient(helper) - if err != nil { - return nil, err - } - - var CSVs operatorsv1alpha1.ClusterServiceVersionList - err = rawClient.List(ctx, &CSVs, client.InNamespace("")) - if err != nil && k8s_errors.IsNotFound(err) { - return nil, nil - } else if err != nil { - return nil, err - } - - for _, CSV := range CSVs.Items { - if strings.HasPrefix(CSV.GetName(), OLSOperatorName) { - return &CSV, nil - } - } - - return nil, nil -} - -// IsUserInstalledOLSOperatorMode checks if an OpenShift Lightspeed Operator -// (OLS Operator) is installed in the cluster (by the user), but was NOT installed/owned by -// this specific OpenStackLightspeed instance. Returns true only if there is an OLS OperatorIsOwnedBy -// ClusterServiceVersion (CSV) found, and that CSV is NOT owned by the given instance. -func IsUserInstalledOLSOperatorMode( - ctx context.Context, - helper *common_helper.Helper, - instance *apiv1beta1.OpenStackLightspeed, -) (bool, error) { - OLSOperatorCSV, err := GetOLSOperatorCSV(ctx, helper) - if err != nil { - return false, err - } else if OLSOperatorCSV == nil { - // Note: If no CSV is found we can be 100 % certain we are in the InstanceOwned - // installation mode. - return false, nil - } - - subscription := &operatorsv1alpha1.Subscription{} - err = helper.GetClient().Get(ctx, client.ObjectKey{ - Name: GetOLSSubscriptionName(instance), - Namespace: instance.Namespace, - }, subscription) - if err != nil && !k8s_errors.IsNotFound(err) { - return false, err - } - - userInstalledMode := !IsOwnedBy(OLSOperatorCSV, instance) && !IsOwnedBy(subscription, instance) - return userInstalledMode, nil -} - -// UninstallInstanceOwnedOLSOperator ensures that the OLS Operator installed by -// a specific OpenStackLightspeed instance is uninstalled from the cluster. The function -// checks if the ClusterServiceVersion (CSV) for the OLS Operator exists and whether it -// is owned by the given OpenStackLightspeed instance. If so, it deletes the CSV. -// The function then checks whether the CSV has been successfully removed. It returns -// true if the operator CSV is no longer found (i.e., uninstalled), or an error if an -// unexpected problem occurs. -func UninstallInstanceOwnedOLSOperator( - ctx context.Context, - helper *common_helper.Helper, - instance *apiv1beta1.OpenStackLightspeed, -) (bool, error) { - OLSOperatorCSV, err := GetOLSOperatorCSV(ctx, helper) - if err != nil { - return false, err - } else if OLSOperatorCSV == nil { - return true, nil - } - - if !IsOwnedBy(OLSOperatorCSV, instance) { - return true, nil - } - - // When the operator is installed via OLM, the OpenStack Lightspeed Subscription - // is also set as an owner of its InstallPlan, resulting in the InstallPlan having - // both the OLS Subscription and the OpenStackLightspeed resources as owners. - // When uninstalling the OLS operator, only the OLS Subscription owner reference is removed, - // which causes the InstallPlans to remain and accumulate over time. To avoid this, - // we explicitly attempt to delete the relevant InstallPlan to prevent leftovers. - _, err = DeleteOLSOperatorInstallPlan(ctx, helper, instance) - if err != nil { - return false, err - } - - if err := helper.GetClient().Delete(ctx, OLSOperatorCSV); err != nil { - return false, err - } - - OLSOperatorCSV, err = GetOLSOperatorCSV(ctx, helper) - if err != nil { - return false, err - } else if OLSOperatorCSV != nil { - return false, nil - } - - OLSInstallPlan, err := GetOLSOperatorInstallPlan(ctx, helper, instance) - if err != nil { - return false, err - } else if OLSInstallPlan != nil { - return false, nil - } - - return true, nil -} - -// GetOLSOperatorInstallPlan returns the InstallPlan that was used to install -// the OpenShift Lightspeed Operator (OLS Operator). It searches for an InstallPlan -// whose ClusterServiceVersion name matches the OLS Operator prefix and the -// recommended OLS version. If such an InstallPlan exists, it is returned; otherwise, -// the function returns nil. -func GetOLSOperatorInstallPlan( - ctx context.Context, - helper *common_helper.Helper, - instance *apiv1beta1.OpenStackLightspeed, -) (*operatorsv1alpha1.InstallPlan, error) { - var installPlans operatorsv1alpha1.InstallPlanList - err := helper.GetClient().List(ctx, &installPlans, client.InNamespace(instance.Namespace)) - if err != nil { - return nil, err - } - - recommendedOLSVersion, err := GetRecommendedOLSVersion() - if err != nil { - return nil, err - } - - for _, installPlan := range installPlans.Items { - var isOLSOperatorCSV bool - for _, csvName := range installPlan.Spec.ClusterServiceVersionNames { - if strings.HasPrefix(csvName, OLSOperatorName) && strings.HasSuffix(csvName, recommendedOLSVersion) { - isOLSOperatorCSV = true - break - } - } - - if isOLSOperatorCSV { - return &installPlan, nil - } - } - - return nil, nil -} - -// ApproveOLSOperatorInstallPlan approves the InstallPlan that is responsible for installing -// the OpenShift Lightspeed Operator (OLS Operator) in the given OpenStackLightspeed instance's -// namespace. It sets the Approved field to true and updates the InstallPlan resource in the cluster. -// Returns true if the approval succeeds, false and an error otherwise. -func ApproveOLSOperatorInstallPlan( - ctx context.Context, - helper *common_helper.Helper, - instance *apiv1beta1.OpenStackLightspeed, -) (bool, error) { - installPlan, err := GetOLSOperatorInstallPlan(ctx, helper, instance) - if err != nil { - return false, err - } else if installPlan == nil { - return false, nil - } - - installPlan.Spec.Approved = true - err = helper.GetClient().Update(ctx, installPlan) - if err != nil { - return false, err - } - - return true, nil -} - -// DeleteOLSOperatorInstallPlan deletes the InstallPlan associated with installing the -// OpenShift Lightspeed Operator (OLS Operator) in the specified OpenStackLightspeed instance's -// namespace. If the InstallPlan does not exist, the function returns true. It returns true -// if the deletion succeeds or the InstallPlan was not found, and false with an error otherwise. -func DeleteOLSOperatorInstallPlan( - ctx context.Context, - helper *common_helper.Helper, - instance *apiv1beta1.OpenStackLightspeed, -) (bool, error) { - installPlan, err := GetOLSOperatorInstallPlan(ctx, helper, instance) - if err != nil { - return false, err - } else if installPlan == nil { - return true, nil - } - - err = helper.GetClient().Delete(ctx, installPlan) - if err != nil && k8s_errors.IsNotFound(err) { - return true, nil - } else if err != nil { - return false, err - } - - return true, nil -} - -// GetOLSSubscriptionName generates a unique subscription name for the OpenStack Lightspeed Operator -// by appending the first 5 characters of the instance's UID. This reduces the likelihood of -// naming collisions with existing subscriptions that may have been created manually by the user. -func GetOLSSubscriptionName(instance *apiv1beta1.OpenStackLightspeed) string { - return fmt.Sprintf("%s-%s", OLSOperatorName, string(instance.GetUID())[:5]) -} - -// SetStartingCSV sets the StartingCSV field of the given Subscription based on -// the recommended OLS operator version. If the recommended version is "", -// StartingCSV is not set to allow OLM to select the latest compatible version. -func SetStartingCSV(subscription *operatorsv1alpha1.Subscription) error { - recommendedVersion, err := GetRecommendedOLSVersion() - if err != nil { - return err - } - - if recommendedVersion != "" { - subscription.Spec.StartingCSV = fmt.Sprintf("%s.v%s", OLSOperatorName, recommendedVersion) - } - - return nil -} diff --git a/internal/controller/openstacklightspeed_controller.go b/internal/controller/openstacklightspeed_controller.go index 67c32dd2..7a87e43b 100644 --- a/internal/controller/openstacklightspeed_controller.go +++ b/internal/controller/openstacklightspeed_controller.go @@ -19,7 +19,6 @@ package controller import ( "context" "fmt" - "time" "github.com/go-logr/logr" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" @@ -56,14 +55,6 @@ func (r *OpenStackLightspeedReconciler) GetLogger(ctx context.Context) logr.Logg // +kubebuilder:rbac:groups=lightspeed.openstack.org,resources=openstacklightspeeds,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=lightspeed.openstack.org,resources=openstacklightspeeds/status,verbs=get;update;patch // +kubebuilder:rbac:groups=lightspeed.openstack.org,resources=openstacklightspeeds/finalizers,verbs=update -// +kubebuilder:rbac:groups=ols.openshift.io,resources=olsconfigs,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=ols.openshift.io,resources=olsconfigs/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=ols.openshift.io,resources=olsconfigs/finalizers,verbs=update -// +kubebuilder:rbac:groups=operators.coreos.com,resources=clusterserviceversions,verbs=get;list;watch -// +kubebuilder:rbac:groups=operators.coreos.com,resources=clusterserviceversions,namespace=openshift-lightspeed,verbs=update;patch;delete -// +kubebuilder:rbac:groups=operators.coreos.com,resources=subscriptions,namespace=openshift-lightspeed,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=operators.coreos.com,resources=installplans,namespace=openshift-lightspeed,verbs=get;list;watch;update;delete -// +kubebuilder:rbac:groups=config.openshift.io,resources=clusterversions,verbs=get;list;watch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -147,7 +138,8 @@ func (r *OpenStackLightspeedReconciler) Reconcile(ctx context.Context, req ctrl. r.resolveOCPVersion(ctx, helper, instance) if !instance.DeletionTimestamp.IsZero() { - return r.reconcileDelete(ctx, helper, instance) + r.reconcileDelete(ctx, helper, instance) + return ctrl.Result{}, nil } if instance.DeletionTimestamp.IsZero() && controllerutil.AddFinalizer(instance, helper.GetFinalizer()) { @@ -162,98 +154,6 @@ func (r *OpenStackLightspeedReconciler) Reconcile(ctx context.Context, req ctrl. instance.Spec.MaxTokensForResponse = apiv1beta1.OpenStackLightspeedDefaultValues.MaxTokensForResponse } - // Ensure a compatible version of the OpenShift Lightspeed Operator is running in the cluster. - // This checks if the correct OLS Operator version is present and installs it if necessary. - isOLSOperatorInstalled, err := EnsureOLSOperatorInstalled(ctx, helper, instance) - if err != nil { - instance.Status.Conditions.Set(condition.FalseCondition( - apiv1beta1.OpenShiftLightspeedOperatorReadyCondition, - condition.ErrorReason, - condition.SeverityWarning, - condition.DeploymentReadyErrorMessage, - err.Error(), - )) - - return ctrl.Result{}, nil - } else if !isOLSOperatorInstalled { - instance.Status.Conditions.Set(condition.FalseCondition( - apiv1beta1.OpenShiftLightspeedOperatorReadyCondition, - condition.RequestedReason, - condition.SeverityInfo, - apiv1beta1.OpenShiftLightspeedOperatorWaiting, - )) - - // In this branch we know that the - return ctrl.Result{Requeue: true, RequeueAfter: 10 * time.Second}, nil - } - - // Mark the OpenShift Lightspeed Operator as ready in the status conditions. - instance.Status.Conditions.MarkTrue( - apiv1beta1.OpenShiftLightspeedOperatorReadyCondition, - apiv1beta1.OpenShiftLightspeedOperatorReady, - ) - - // NOTE: We cannot consume the OLSConfig definition directly from the OLS operator's code due to - // a conflict in Go versions. When this comment was written, the min. required Go version for - // openstack-operator was 1.21 whereas OLS operator required at least Go version 1.23. Once the - // Go versions catch up with each other we should consider consuming OLSConfig directly from OLS - // operator and updating this code and any subsequent code that consumes this structure. - olsConfig := uns.Unstructured{} - olsConfigGVK := schema.GroupVersionKind{ - Group: "ols.openshift.io", - Version: "v1alpha1", - Kind: "OLSConfig", - } - - olsConfig.SetGroupVersionKind(olsConfigGVK) - olsConfig.SetName(OLSConfigName) - - _, err = controllerutil.CreateOrPatch(ctx, r.Client, &olsConfig, func() error { - // Check if the OpenStackLightspeed instance that is being processed owns the OLSConfig. If - // it is owned by other OpenStackLightspeed instance stop the reconciliation. - olsConfigLabels := olsConfig.GetLabels() - ownerLabel := "" - if val, ok := olsConfigLabels[OpenStackLightspeedOwnerIDLabel]; ok { - ownerLabel = val - } - - if ownerLabel != "" && ownerLabel != string(instance.GetObjectMeta().GetUID()) { - return fmt.Errorf("OLSConfig is managed by different OpenStackLightspeed instance") - } - - err = PatchOLSConfig(helper, instance, &olsConfig) - if err != nil { - return err - } - - return nil - }) - if err != nil { - instance.Status.Conditions.Set(condition.FalseCondition( - apiv1beta1.OpenStackLightspeedReadyCondition, - condition.ErrorReason, - condition.SeverityWarning, - condition.DeploymentReadyErrorMessage, - err.Error())) - return ctrl.Result{}, err - } - - OLSConfigReady, err := IsOLSConfigReady(ctx, helper) - if err != nil { - return ctrl.Result{}, err - } - - if OLSConfigReady { - instance.Status.Conditions.MarkTrue( - apiv1beta1.OpenStackLightspeedReadyCondition, - apiv1beta1.OpenStackLightspeedReadyMessage, - ) - Log.Info("OLSConfig is ready!") - } else { - Log.Info("OLSConfig is not ready yet. Waiting...") - return ctrl.Result{RequeueAfter: time.Second * time.Duration(5)}, nil - } - Log.Info("OpenStackLightspeed Reconciled successfully") return ctrl.Result{}, nil } @@ -351,30 +251,13 @@ func (r *OpenStackLightspeedReconciler) reconcileDelete( ctx context.Context, helper *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed, -) (ctrl.Result, error) { +) { Log := r.GetLogger(ctx) Log.Info("OpenStackLightspeed Reconciling Delete") - isRemoved, err := RemoveOLSConfig(ctx, helper, instance) - if err != nil { - return ctrl.Result{}, err - } else if !isRemoved { - Log.Info("OLSConfig removal in progress ...") - return ctrl.Result{RequeueAfter: time.Second * 10}, nil - } - - isUninstalled, err := UninstallInstanceOwnedOLSOperator(ctx, helper, instance) - if err != nil { - return ctrl.Result{}, err - } else if !isUninstalled { - Log.Info("OLS Operator uninstallation in progress ...") - return ctrl.Result{RequeueAfter: time.Second * 10}, nil - } - controllerutil.RemoveFinalizer(instance, helper.GetFinalizer()) Log.Info("OpenStackLightspeed Reconciling Delete completed") - return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. diff --git a/scripts/env.sh b/scripts/env.sh index 3b361302..9d1f99df 100644 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -1,3 +1,2 @@ #!/bin/bash -export OPENSHIFT_LIGHTSPEED_OPERATOR_VERSION="latest" export WATCH_NAMESPACE="openshift-lightspeed" diff --git a/scripts/gen-ols.sh b/scripts/gen-ols.sh deleted file mode 100644 index b4e9be64..00000000 --- a/scripts/gen-ols.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -# Deploy OLS -# Optionally pass the CSV version to use as an argument to use other than the -# latest stable version. -set -ex - -if [ -z "${OUTPUT_DIR}" ]; then - echo "Please set OPERATOR_DIR" - exit 1 -fi - -if [ -n "$1" ]; then - CSV_VERSION="$1" -else - CSV_VERSION=$(oc get packagemanifest lightspeed-operator -o go-template="{{range .status.channels}}{{if eq .name \"stable\"}}{{.currentCSV}}{{\"\n\"}}{{end}}{{end}}") -fi - -DEST_DIR="${OUTPUT_DIR}/ols" -mkdir -p "${DEST_DIR}" - -cat > "${DEST_DIR}/namespace.yaml" < "${DEST_DIR}/operator_group.yaml" < "${DEST_DIR}/subscription.yaml" <