diff --git a/Makefile b/Makefile index beaa11209..8f8ed2956 100644 --- a/Makefile +++ b/Makefile @@ -137,7 +137,7 @@ help: ## Display this help. manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. mkdir -p config/operator/rbac && \ $(CONTROLLER_GEN) crd$(CRDDESC_OVERRIDE) output:crd:artifacts:config=config/crd/bases webhook paths="./..." && \ - $(CONTROLLER_GEN) rbac:roleName=manager-role paths="{./apis/client/...,./apis/core/...,./apis/dataplane/...,./controllers/client/...,./controllers/core/...,./controllers/dataplane/...,./pkg/...}" output:dir=config/rbac && \ + $(CONTROLLER_GEN) rbac:roleName=manager-role paths="{./apis/lightspeed/...,./apis/client/...,./apis/core/...,./apis/dataplane/...,./controllers/lightspeed/...,./controllers/client/...,./controllers/core/...,./controllers/dataplane/...,./pkg/...}" output:dir=config/rbac && \ $(CONTROLLER_GEN) rbac:roleName=operator-role paths="./controllers/operator/..." paths="./apis/operator/..." output:dir=config/operator/rbac && \ rm -f apis/bases/* && cp -a config/crd/bases apis/ diff --git a/PROJECT b/PROJECT index 3a611a529..3868e8653 100644 --- a/PROJECT +++ b/PROJECT @@ -86,4 +86,13 @@ resources: kind: OpenStack path: github.com/openstack-k8s-operators/openstack-operator/apis/operator/v1beta1 version: v1beta1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: openstack.org + group: lightspeed + kind: OpenStackLightspeed + path: github.com/openstack-k8s-operators/openstack-operator/apis/lightspeed/v1beta1 + version: v1beta1 version: "3" diff --git a/apis/bases/lightspeed.openstack.org_openstacklightspeeds.yaml b/apis/bases/lightspeed.openstack.org_openstacklightspeeds.yaml new file mode 100644 index 000000000..104850713 --- /dev/null +++ b/apis/bases/lightspeed.openstack.org_openstacklightspeeds.yaml @@ -0,0 +1,96 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: openstacklightspeeds.lightspeed.openstack.org +spec: + group: lightspeed.openstack.org + names: + kind: OpenStackLightspeed + listKind: OpenStackLightspeedList + plural: openstacklightspeeds + singular: openstacklightspeed + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Status + jsonPath: .status.conditions[0].status + name: Status + type: string + - description: Message + jsonPath: .status.conditions[0].message + name: Message + type: string + name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + llmCredentials: + type: string + llmEndpoint: + type: string + llmEndpointType: + enum: + - azure_openai + - bam + - openai + - watsonx + - rhoai_vllm + - rhelai_vllm + - fake_provider + type: string + modelName: + type: string + ragImage: + type: string + tlsCACertBundle: + type: string + required: + - llmCredentials + - llmEndpoint + - llmEndpointType + - modelName + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + severity: + type: string + status: + type: string + type: + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + observedGeneration: + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/apis/lightspeed/v1beta1/conditions.go b/apis/lightspeed/v1beta1/conditions.go new file mode 100644 index 000000000..c19ecd23e --- /dev/null +++ b/apis/lightspeed/v1beta1/conditions.go @@ -0,0 +1,40 @@ +/* +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. +*/ + +package v1beta1 + +import ( + condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" +) + +// OpenStackLightspeed Condition Types used by API objects. +const ( + // OpenStackLightspeedReadyCondition Status=True condition which indicates if OpenStackLightspeedReadyCondition + // is configured and operational + OpenStackLightspeedReadyCondition condition.Type = "OpenStackLightspeedReady" +) + +// Common Messages used by API objects. +const ( + // OpenStackLightspeedReadyInitMessage + OpenStackLightspeedReadyInitMessage = "OpenStack Lightspeed not started" + + // OpenStackLightspeedReadyMessage + OpenStackLightspeedReadyMessage = "OpenStack Lightspeed created" + + // OpenStackLightspeedWaitingVectorDBMessage + OpenStackLightspeedWaitingVectorDBMessage = "Waiting for OpenStackLightspeed vector DB pod to become ready" +) diff --git a/apis/lightspeed/v1beta1/groupversion_info.go b/apis/lightspeed/v1beta1/groupversion_info.go new file mode 100644 index 000000000..aa206501c --- /dev/null +++ b/apis/lightspeed/v1beta1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +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. +*/ + +// Package v1beta1 contains API Schema definitions for the lightspeed v1beta1 API group +// +kubebuilder:object:generate=true +// +groupName=lightspeed.openstack.org +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "lightspeed.openstack.org", Version: "v1beta1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/apis/lightspeed/v1beta1/openstacklightspeed_types.go b/apis/lightspeed/v1beta1/openstacklightspeed_types.go new file mode 100644 index 000000000..c9250eec1 --- /dev/null +++ b/apis/lightspeed/v1beta1/openstacklightspeed_types.go @@ -0,0 +1,124 @@ +/* +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. +*/ + +package v1beta1 + +import ( + "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + "github.com/openstack-k8s-operators/lib-common/modules/common/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // Container image fall-back defaults + + // OpenStackLightspeedContainerImage is the fall-back container image for OpenStackLightspeed + OpenStackLightspeedContainerImage = "quay.io/openstack-lightspeed/rag-content:os-docs-2024.2" +) + +// OpenStackLightspeedSpec defines the desired state of OpenStackLightspeed +type OpenStackLightspeedSpec struct { + OpenStackLightspeedCore `json:",inline"` + + // +kubebuilder:validation:Optional + // ContainerImage for the Openstack Lightspeed RAG container (will be set to environmental default if empty) + RAGImage string `json:"ragImage"` +} + +// OpenStackLightspeedCore defines the desired state of OpenStackLightspeed +type OpenStackLightspeedCore struct { + // +kubebuilder:validation:Required + // URL pointing to the LLM + LLMEndpoint string `json:"llmEndpoint"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=azure_openai;bam;openai;watsonx;rhoai_vllm;rhelai_vllm;fake_provider + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Provider Type" + // Type of the provider serving the LLM + LLMEndpointType string `json:"llmEndpointType"` + + // +kubebuilder:validation:Required + // Name of the model to use at the API endpoint provided in LLMEndpoint + ModelName string `json:"modelName"` + + // +kubebuilder:validation:Required + // Secret name containing API token for the LLMEndpoint. The key for the field + // in the secret that holds the token should be "apitoken". + LLMCredentials string `json:"llmCredentials"` + + // +kubebuilder:validation:Optional + // Configmap name containing a CA Certificates bundle + TLSCACertBundle string `json:"tlsCACertBundle"` +} + +// OpenStackLightspeedStatus defines the observed state of OpenStackLightspeed +type OpenStackLightspeedStatus struct { + // Conditions + Conditions condition.Conditions `json:"conditions,omitempty" optional:"true"` + + // ObservedGeneration - the most recent generation observed for this object. + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +operator-sdk:csv:customresourcedefinitions:displayName="OpenStack Lightspeed" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[0].status",description="Status" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[0].message",description="Message" + +// OpenStackLightspeed is the Schema for the openstacklightspeeds API +type OpenStackLightspeed struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OpenStackLightspeedSpec `json:"spec,omitempty"` + Status OpenStackLightspeedStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// OpenStackLightspeedList contains a list of OpenStackLightspeed +type OpenStackLightspeedList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OpenStackLightspeed `json:"items"` +} + +func init() { + SchemeBuilder.Register(&OpenStackLightspeed{}, &OpenStackLightspeedList{}) +} + +// IsReady - returns true if OpenStackLightspeed is reconciled successfully +func (instance OpenStackLightspeed) IsReady() bool { + return instance.Status.Conditions.IsTrue(OpenStackLightspeedReadyCondition) +} + +type OpenStackLightspeedDefaults struct { + RAGImageURL string +} + +var OpenStackLightspeedDefaultValues OpenStackLightspeedDefaults + +// SetupDefaults - initializes OpenStackLightspeedDefaultValues with default values from env vars +func SetupDefaults() { + // Acquire environmental defaults and initialize OpenStackLightspeed defaults with them + openStackLightspeedDefaults := OpenStackLightspeedDefaults{ + RAGImageURL: util.GetEnvVar( + "RELATED_IMAGE_OPENSTACK_LIGHTSPEED_IMAGE_URL_DEFAULT", OpenStackLightspeedContainerImage), + } + + OpenStackLightspeedDefaultValues = openStackLightspeedDefaults +} diff --git a/apis/lightspeed/v1beta1/webhook_suite_test.go b/apis/lightspeed/v1beta1/webhook_suite_test.go new file mode 100644 index 000000000..cd57b7be3 --- /dev/null +++ b/apis/lightspeed/v1beta1/webhook_suite_test.go @@ -0,0 +1,28 @@ +/* +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. +*/ + +package v1beta1 + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "testing" +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "LightSpeed v1beta1 Suite") +} diff --git a/apis/lightspeed/v1beta1/zz_generated.deepcopy.go b/apis/lightspeed/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 000000000..571eb4232 --- /dev/null +++ b/apis/lightspeed/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,153 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2022. + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackLightspeed) DeepCopyInto(out *OpenStackLightspeed) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackLightspeed. +func (in *OpenStackLightspeed) DeepCopy() *OpenStackLightspeed { + if in == nil { + return nil + } + out := new(OpenStackLightspeed) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenStackLightspeed) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackLightspeedCore) DeepCopyInto(out *OpenStackLightspeedCore) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackLightspeedCore. +func (in *OpenStackLightspeedCore) DeepCopy() *OpenStackLightspeedCore { + if in == nil { + return nil + } + out := new(OpenStackLightspeedCore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackLightspeedDefaults) DeepCopyInto(out *OpenStackLightspeedDefaults) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackLightspeedDefaults. +func (in *OpenStackLightspeedDefaults) DeepCopy() *OpenStackLightspeedDefaults { + if in == nil { + return nil + } + out := new(OpenStackLightspeedDefaults) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackLightspeedList) DeepCopyInto(out *OpenStackLightspeedList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OpenStackLightspeed, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackLightspeedList. +func (in *OpenStackLightspeedList) DeepCopy() *OpenStackLightspeedList { + if in == nil { + return nil + } + out := new(OpenStackLightspeedList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenStackLightspeedList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackLightspeedSpec) DeepCopyInto(out *OpenStackLightspeedSpec) { + *out = *in + out.OpenStackLightspeedCore = in.OpenStackLightspeedCore +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackLightspeedSpec. +func (in *OpenStackLightspeedSpec) DeepCopy() *OpenStackLightspeedSpec { + if in == nil { + return nil + } + out := new(OpenStackLightspeedSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackLightspeedStatus) DeepCopyInto(out *OpenStackLightspeedStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(condition.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackLightspeedStatus. +func (in *OpenStackLightspeedStatus) DeepCopy() *OpenStackLightspeedStatus { + if in == nil { + return nil + } + out := new(OpenStackLightspeedStatus) + in.DeepCopyInto(out) + return out +} diff --git a/bindata/crds/crds.yaml b/bindata/crds/crds.yaml index 7d810d6fe..9e0507764 100644 --- a/bindata/crds/crds.yaml +++ b/bindata/crds/crds.yaml @@ -17693,6 +17693,102 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: openstacklightspeeds.lightspeed.openstack.org +spec: + group: lightspeed.openstack.org + names: + kind: OpenStackLightspeed + listKind: OpenStackLightspeedList + plural: openstacklightspeeds + singular: openstacklightspeed + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Status + jsonPath: .status.conditions[0].status + name: Status + type: string + - description: Message + jsonPath: .status.conditions[0].message + name: Message + type: string + name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + llmCredentials: + type: string + llmEndpoint: + type: string + llmEndpointType: + enum: + - azure_openai + - bam + - openai + - watsonx + - rhoai_vllm + - rhelai_vllm + - fake_provider + type: string + modelName: + type: string + ragImage: + type: string + tlsCACertBundle: + type: string + required: + - llmCredentials + - llmEndpoint + - llmEndpointType + - modelName + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + severity: + type: string + status: + type: string + type: + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + observedGeneration: + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 diff --git a/bindata/rbac/rbac.yaml b/bindata/rbac/rbac.yaml index 09a3fd41b..17b067e05 100644 --- a/bindata/rbac/rbac.yaml +++ b/bindata/rbac/rbac.yaml @@ -79,6 +79,7 @@ rules: - "" resources: - namespaces + - pods/log - projects verbs: - get @@ -393,6 +394,32 @@ rules: - patch - update - watch +- apiGroups: + - lightspeed.openstack.org + resources: + - openstacklightspeeds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - lightspeed.openstack.org + resources: + - openstacklightspeeds/finalizers + verbs: + - update +- apiGroups: + - lightspeed.openstack.org + resources: + - openstacklightspeeds/status + verbs: + - get + - patch + - update - apiGroups: - machineconfiguration.openshift.io resources: @@ -511,6 +538,32 @@ rules: - patch - update - watch +- 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: - operator.openshift.io resources: @@ -519,6 +572,13 @@ rules: - get - list - watch +- apiGroups: + - operators.coreos.com + resources: + - clusterserviceversions + verbs: + - get + - list - apiGroups: - ovn.openstack.org resources: diff --git a/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml b/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml new file mode 100644 index 000000000..104850713 --- /dev/null +++ b/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml @@ -0,0 +1,96 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: openstacklightspeeds.lightspeed.openstack.org +spec: + group: lightspeed.openstack.org + names: + kind: OpenStackLightspeed + listKind: OpenStackLightspeedList + plural: openstacklightspeeds + singular: openstacklightspeed + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Status + jsonPath: .status.conditions[0].status + name: Status + type: string + - description: Message + jsonPath: .status.conditions[0].message + name: Message + type: string + name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + llmCredentials: + type: string + llmEndpoint: + type: string + llmEndpointType: + enum: + - azure_openai + - bam + - openai + - watsonx + - rhoai_vllm + - rhelai_vllm + - fake_provider + type: string + modelName: + type: string + ragImage: + type: string + tlsCACertBundle: + type: string + required: + - llmCredentials + - llmEndpoint + - llmEndpointType + - modelName + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + severity: + type: string + status: + type: string + type: + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + observedGeneration: + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index d9616b021..99115b878 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -9,6 +9,7 @@ resources: - bases/dataplane.openstack.org_openstackdataplaneservices.yaml - bases/dataplane.openstack.org_openstackdataplanedeployments.yaml #- bases/operator.openstack.org_openstacks.yaml +- bases/lightspeed.openstack.org_openstacklightspeeds.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: @@ -26,6 +27,7 @@ patches: #- path: patches/cainjection_in_client_openstackclients.yaml #- path: patches/cainjection_in_core_openstackversions.yaml #- path: patches/cainjection_in_operator_openstacks.yaml +#- path: patches/cainjection_in_openstacklightspeeds.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_lightspeed_openstacklightspeeds.yaml b/config/crd/patches/cainjection_in_lightspeed_openstacklightspeeds.yaml new file mode 100644 index 000000000..4baf3e4af --- /dev/null +++ b/config/crd/patches/cainjection_in_lightspeed_openstacklightspeeds.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: openstacklightspeeds.lightspeed.openstack.org diff --git a/config/manifests/bases/openstack-operator.clusterserviceversion.yaml b/config/manifests/bases/openstack-operator.clusterserviceversion.yaml index a78a4a5da..4b159c1bb 100644 --- a/config/manifests/bases/openstack-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/openstack-operator.clusterserviceversion.yaml @@ -633,6 +633,16 @@ spec: x-descriptors: - urn:alm:descriptor:io.kubernetes.conditions version: v1beta1 + - description: OpenStackLightspeed is the Schema for the openstacklightspeeds + API + displayName: OpenStack Lightspeed + kind: OpenStackLightspeed + name: openstacklightspeeds.lightspeed.openstack.org + specDescriptors: + - description: Type of the provider serving the LLM + displayName: Provider Type + path: llmEndpointType + version: v1beta1 - description: OpenStack is the Schema for the openstacks API displayName: OpenStack kind: OpenStack diff --git a/config/operator/default_images.yaml b/config/operator/default_images.yaml index e67128b25..b768e9452 100644 --- a/config/operator/default_images.yaml +++ b/config/operator/default_images.yaml @@ -187,6 +187,8 @@ spec: value: quay.io/podified-master-centos9/openstack-watcher-applier:current-podified - name: RELATED_IMAGE_WATCHER_DECISION_ENGINE_IMAGE_URL_DEFAULT value: quay.io/podified-master-centos9/openstack-watcher-decision-engine:current-podified + - name: RELATED_IMAGE_OPENSTACK_LIGHTSPEED_IMAGE_URL_DEFAULT + value: quay.io/openstack-lightspeed/rag-content:os-docs-2024.2 # NOTE: TEST_ images below do not get released downstream. They should not be prefixed with RELATED - name: TEST_TOBIKO_IMAGE_URL_DEFAULT value: quay.io/podified-antelope-centos9/openstack-tobiko:current-podified diff --git a/config/rbac/lightspeed_openstacklightspeed_editor_role.yaml b/config/rbac/lightspeed_openstacklightspeed_editor_role.yaml new file mode 100644 index 000000000..ee4963be5 --- /dev/null +++ b/config/rbac/lightspeed_openstacklightspeed_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit openstacklightspeeds. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: openstacklightspeed-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: openstack-operator + app.kubernetes.io/part-of: openstack-operator + app.kubernetes.io/managed-by: kustomize + name: openstacklightspeed-editor-role +rules: +- apiGroups: + - lightspeed.openstack.org + resources: + - openstacklightspeeds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - lightspeed.openstack.org + resources: + - openstacklightspeeds/status + verbs: + - get diff --git a/config/rbac/lightspeed_openstacklightspeed_viewer_role.yaml b/config/rbac/lightspeed_openstacklightspeed_viewer_role.yaml new file mode 100644 index 000000000..019e8a408 --- /dev/null +++ b/config/rbac/lightspeed_openstacklightspeed_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view openstacklightspeeds. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: openstacklightspeed-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: openstack-operator + app.kubernetes.io/part-of: openstack-operator + app.kubernetes.io/managed-by: kustomize + name: openstacklightspeed-viewer-role +rules: +- apiGroups: + - lightspeed.openstack.org + resources: + - openstacklightspeeds + verbs: + - get + - list + - watch +- apiGroups: + - lightspeed.openstack.org + resources: + - openstacklightspeeds/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 7decfd254..d970de4fc 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -36,6 +36,7 @@ rules: - "" resources: - namespaces + - pods/log - projects verbs: - get @@ -350,6 +351,32 @@ rules: - patch - update - watch +- apiGroups: + - lightspeed.openstack.org + resources: + - openstacklightspeeds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - lightspeed.openstack.org + resources: + - openstacklightspeeds/finalizers + verbs: + - update +- apiGroups: + - lightspeed.openstack.org + resources: + - openstacklightspeeds/status + verbs: + - get + - patch + - update - apiGroups: - machineconfiguration.openshift.io resources: @@ -468,6 +495,32 @@ rules: - patch - update - watch +- 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: - operator.openshift.io resources: @@ -476,6 +529,13 @@ rules: - get - list - watch +- apiGroups: + - operators.coreos.com + resources: + - clusterserviceversions + verbs: + - get + - list - apiGroups: - ovn.openstack.org resources: diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 138d15b6b..966b2505e 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -10,4 +10,5 @@ resources: #- dataplane_v1beta1_openstackdataplaneservice_empty.yaml #- dataplane_v1beta1_openstackdataplanedeployment_empty.yaml - operator_v1beta1_openstack.yaml +- lightspeed_v1beta1_openstacklightspeed.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml b/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml new file mode 100644 index 000000000..6b0293801 --- /dev/null +++ b/config/samples/lightspeed_v1beta1_openstacklightspeed.yaml @@ -0,0 +1,10 @@ +apiVersion: lightspeed.openstack.org/v1beta1 +kind: OpenStackLightspeed +metadata: + name: openstacklightspeed-sample +spec: + llmEndpoint: + llmEndpointType: + llmCredentials: + modelName: + tlsCACertBundle: diff --git a/controllers/lightspeed/openstacklightspeed_controller.go b/controllers/lightspeed/openstacklightspeed_controller.go new file mode 100644 index 000000000..56b1dd726 --- /dev/null +++ b/controllers/lightspeed/openstacklightspeed_controller.go @@ -0,0 +1,319 @@ +/* +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. +*/ + +// Package lightspeed implements the OpenStackLightspeed controller for managing OpenStack Lightspeed resources +package lightspeed + +import ( + "context" + "fmt" + "time" + + corev1beta1 "github.com/openstack-k8s-operators/openstack-operator/apis/core/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/go-logr/logr" + "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + lightspeedv1 "github.com/openstack-k8s-operators/openstack-operator/apis/lightspeed/v1beta1" + k8s_errors "k8s.io/apimachinery/pkg/api/errors" + uns "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/openstack-k8s-operators/openstack-operator/pkg/lightspeed" +) + +// OpenStackLightspeedReconciler reconciles a OpenStackLightspeed object +type OpenStackLightspeedReconciler struct { + client.Client + Scheme *runtime.Scheme + Kclient kubernetes.Interface +} + +// GetLogger returns a logger object with a prefix of "controller.name" and additional controller context fields +func (r *OpenStackLightspeedReconciler) GetLogger(ctx context.Context) logr.Logger { + return log.FromContext(ctx).WithName("Controllers").WithName("OpenStackLightspeed") +} + +// +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; +// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete; +// +kubebuilder:rbac:groups="",resources=pods,verbs=create;delete;get;list;patch;update;watch +// +kubebuilder:rbac:groups="",resources=pods/log,verbs=get + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.1/pkg/reconcile +func (r *OpenStackLightspeedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + Log := r.GetLogger(ctx) + + instance := &lightspeedv1.OpenStackLightspeed{} + err := r.Get(ctx, req.NamespacedName, instance) + if err != nil { + if k8s_errors.IsNotFound(err) { + Log.Info("OpenStackLightspeed CR not found") + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + helper, err := common_helper.NewHelper( + instance, + r.Client, + r.Kclient, + r.Scheme, + Log, + ) + + // Save a copy of the conditions so that we can restore the LastTransitionTime + // when a condition's state doesn't change. + savedConditions := instance.Status.Conditions.DeepCopy() + + // Always patch the instance status when exiting this function so we can persist any changes. + defer func() { + // Don't update the status, if reconciler Panics + if r := recover(); r != nil { + Log.Info(fmt.Sprintf("panic during reconcile %v\n", r)) + panic(r) + } + + condition.RestoreLastTransitionTimes(&instance.Status.Conditions, savedConditions) + // update the Ready condition based on the sub conditions + if instance.Status.Conditions.AllSubConditionIsTrue() { + instance.Status.Conditions.MarkTrue( + condition.ReadyCondition, condition.ReadyMessage) + } else { + // something is not ready so reset the Ready condition + instance.Status.Conditions.MarkUnknown( + condition.ReadyCondition, condition.InitReason, condition.ReadyInitMessage) + // and recalculate it based on the state of the rest of the conditions + instance.Status.Conditions.Set( + instance.Status.Conditions.Mirror(condition.ReadyCondition)) + } + + err := helper.PatchInstance(ctx, instance) + if err != nil { + return + } + + }() + + cl := condition.CreateList( + condition.UnknownCondition( + lightspeedv1.OpenStackLightspeedReadyCondition, + condition.InitReason, + lightspeedv1.OpenStackLightspeedReadyInitMessage, + ), + ) + + instance.Status.Conditions.Init(&cl) + instance.Status.ObservedGeneration = instance.Generation + + if !instance.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, helper, instance) + } + + if instance.DeletionTimestamp.IsZero() && controllerutil.AddFinalizer(instance, helper.GetFinalizer()) { + return ctrl.Result{}, nil + } + + if instance.Spec.RAGImage == "" { + instance.Spec.RAGImage = lightspeedv1.OpenStackLightspeedDefaultValues.RAGImageURL + } + + OLSOperatorInstalled, err := lightspeed.IsOLSOperatorInstalled(ctx, helper) + if !OLSOperatorInstalled || err != nil { + errMsg := fmt.Errorf("installation of OpenShift LightSpeed not detected") + instance.Status.Conditions.Set(condition.FalseCondition( + lightspeedv1.OpenStackLightspeedReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.DeploymentReadyErrorMessage, + errMsg)) + return ctrl.Result{}, errMsg + } + + // TODO(lpiwowar): Remove ResolveIndexID once OpenShift Lightspeed supports auto discovery of the indexID directly + // from the vector db image. + indexID, result, err := lightspeed.ResolveIndexID(ctx, helper, instance) + if err != nil { + instance.Status.Conditions.Set(condition.FalseCondition( + lightspeedv1.OpenStackLightspeedReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.DeploymentReadyErrorMessage, + err.Error())) + return result, err + } else if (result != ctrl.Result{}) { + return result, nil + } + + // 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(lightspeed.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[lightspeed.OpenStackLightspeedOwnerIDLabel]; ok { + ownerLabel = val + } + + if ownerLabel != "" && ownerLabel != string(instance.GetObjectMeta().GetUID()) { + return fmt.Errorf("OLSConfig is managed by different OpenStackLightspeed instance") + } + + err = lightspeed.PatchOLSConfig(helper, instance, &olsConfig, indexID) + if err != nil { + return err + } + + return nil + }) + if err != nil { + instance.Status.Conditions.Set(condition.FalseCondition( + lightspeedv1.OpenStackLightspeedReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.DeploymentReadyErrorMessage, + err.Error())) + return ctrl.Result{}, err + } + + OLSConfigReady, err := lightspeed.IsOLSConfigReady(ctx, helper) + if err != nil { + return ctrl.Result{}, err + } + + if OLSConfigReady { + instance.Status.Conditions.MarkTrue( + lightspeedv1.OpenStackLightspeedReadyCondition, + lightspeedv1.OpenStackLightspeedReadyMessage, + ) + } else { + Log.Info("OLSConfig is not ready yet. Waiting.") + return ctrl.Result{RequeueAfter: time.Second * time.Duration(5)}, nil + } + + return ctrl.Result{}, nil +} + +// reconcileDelete reconciles the deletion of OpenStackLightspeed instance +func (r *OpenStackLightspeedReconciler) reconcileDelete( + ctx context.Context, + helper *common_helper.Helper, + instance *lightspeedv1.OpenStackLightspeed, +) (ctrl.Result, error) { + Log := r.GetLogger(ctx) + + olsConfig, err := lightspeed.GetOLSConfig(ctx, helper) + if err != nil && k8s_errors.IsNotFound(err) { + controllerutil.RemoveFinalizer(instance, helper.GetFinalizer()) + return ctrl.Result{}, nil + } else if err != nil { + return ctrl.Result{}, err + } + + ownerLabel := olsConfig.GetLabels()[lightspeed.OpenStackLightspeedOwnerIDLabel] + if ownerLabel == "" || ownerLabel != string(instance.GetObjectMeta().GetUID()) { + Log.Info("Skipping OLSConfig deletion as it is not managed by the OpenStackLightspeed instance") + controllerutil.RemoveFinalizer(instance, helper.GetFinalizer()) + return ctrl.Result{}, nil + } + + _, err = controllerutil.CreateOrPatch(ctx, r.Client, &olsConfig, func() error { + if ok := controllerutil.RemoveFinalizer(&olsConfig, helper.GetFinalizer()); !ok { + return fmt.Errorf("remove finalizer failed") + } + + return nil + }) + if err != nil { + return ctrl.Result{}, err + } + + err = r.Delete(ctx, &olsConfig) + if err != nil { + return ctrl.Result{}, err + } + + controllerutil.RemoveFinalizer(instance, helper.GetFinalizer()) + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *OpenStackLightspeedReconciler) SetupWithManager(mgr ctrl.Manager) error { + versionFunc := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request { + Log := r.GetLogger(ctx) + versionList := &corev1beta1.OpenStackVersionList{} + + var result []reconcile.Request + + listOpts := []client.ListOption{ + client.InNamespace(o.GetNamespace()), + } + if err := r.List(ctx, versionList, listOpts...); err != nil { + Log.Error(err, "Unable to retrieve OpenStackVersion") + return nil + } + + for _, i := range versionList.Items { + name := client.ObjectKey{ + Namespace: o.GetNamespace(), + Name: i.Name, + } + result = append(result, reconcile.Request{NamespacedName: name}) + } + if len(result) > 0 { + Log.Info("Reconcile request for:", "result", result) + return result + } + return nil + }) + + return ctrl.NewControllerManagedBy(mgr). + For(&lightspeedv1.OpenStackLightspeed{}). + Watches(&corev1beta1.OpenStackVersion{}, versionFunc). + Complete(r) +} diff --git a/hack/export_related_images.sh b/hack/export_related_images.sh index 096966d2e..f15a20155 100755 --- a/hack/export_related_images.sh +++ b/hack/export_related_images.sh @@ -90,6 +90,7 @@ export RELATED_IMAGE_TEST_TEMPEST_IMAGE_URL_DEFAULT=quay.io/podified-antelope-ce export RELATED_IMAGE_WATCHER_API_IMAGE_URL_DEFAULT=quay.io/podified-master-centos9/openstack-watcher-api:current-podified export RELATED_IMAGE_WATCHER_APPLIER_IMAGE_URL_DEFAULT=quay.io/podified-master-centos9/openstack-watcher-applier:current-podified export RELATED_IMAGE_WATCHER_DECISION_ENGINE_IMAGE_URL_DEFAULT=quay.io/podified-master-centos9/openstack-watcher-decision-engine:current-podified +export RELATED_IMAGE_OPENSTACK_LIGHTSPEED_IMAGE_URL_DEFAULT=quay.io/openstack-lightspeed/rag-content:os-docs-2024.2 #NOTE: TEST_ images below do not get released downstream. They should not be prefixed with RELATED export TEST_TOBIKO_IMAGE_URL_DEFAULT=quay.io/podified-antelope-centos9/openstack-tobiko:current-podified export TEST_ANSIBLETEST_IMAGE_URL_DEFAULT=quay.io/podified-antelope-centos9/openstack-ansible-tests:current-podified diff --git a/main.go b/main.go index bd0cbbf8a..6e2516c53 100644 --- a/main.go +++ b/main.go @@ -86,9 +86,11 @@ import ( machineconfig "github.com/openshift/api/machineconfiguration/v1" ocp_image "github.com/openshift/api/operator/v1alpha1" + lightspeedv1beta1 "github.com/openstack-k8s-operators/openstack-operator/apis/lightspeed/v1beta1" clientcontrollers "github.com/openstack-k8s-operators/openstack-operator/controllers/client" corecontrollers "github.com/openstack-k8s-operators/openstack-operator/controllers/core" dataplanecontrollers "github.com/openstack-k8s-operators/openstack-operator/controllers/dataplane" + lightspeedcontrollers "github.com/openstack-k8s-operators/openstack-operator/controllers/lightspeed" "github.com/openstack-k8s-operators/openstack-operator/pkg/openstack" // +kubebuilder:scaffold:imports ) @@ -135,6 +137,7 @@ func init() { utilruntime.Must(operatorv1beta1.AddToScheme(scheme)) utilruntime.Must(topologyv1.AddToScheme(scheme)) utilruntime.Must(watcherv1.AddToScheme(scheme)) + utilruntime.Must(lightspeedv1beta1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } @@ -277,6 +280,9 @@ func main() { // Defaults for OpenStackClient clientv1.SetupDefaults() + // Defaults for OpenStackLightspeed + lightspeedv1beta1.SetupDefaults() + // Defaults for Dataplane dataplanev1.SetupDefaults() @@ -314,6 +320,14 @@ func main() { checker = mgr.GetWebhookServer().StartedChecker() } + if err = (&lightspeedcontrollers.OpenStackLightspeedReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "OpenStackLightspeed") + os.Exit(1) + } + // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", checker); err != nil { setupLog.Error(err, "unable to set up health check") diff --git a/pkg/lightspeed/funcs.go b/pkg/lightspeed/funcs.go new file mode 100644 index 000000000..ccab886fd --- /dev/null +++ b/pkg/lightspeed/funcs.go @@ -0,0 +1,383 @@ +/* +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. +*/ + +// Package lightspeed provides utilities and functions for OpenStack Lightspeed operations +package lightspeed + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + batchv1 "k8s.io/api/batch/v1" + "k8s.io/client-go/kubernetes" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" + + common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + lightspeedv1 "github.com/openstack-k8s-operators/openstack-operator/apis/lightspeed/v1beta1" + corev1 "k8s.io/api/core/v1" + 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 ( + // OpenStackLightspeedDefaultProvider - contains default name for the provider created in OLSConfig + // by openstack-operator. + OpenStackLightspeedDefaultProvider = "openstack-lightspeed-provider" + + // OpenStackLightspeedOwnerIDLabel - name of a label that contains ID of OpenStackLightspeed instance + // that manages the OLSConfig. + OpenStackLightspeedOwnerIDLabel = "openstack.org/lightspeed-owner-id" + + // OpenStackLightspeedVectorDBPath - path inside of the container image where the vector DB are + // located + OpenStackLightspeedVectorDBPath = "/rag/vector_db/os_product_docs" + + // OpenStackLightspeedJobName - name of the pod that is used to discover environment variables inside of the RAG + // container image + OpenStackLightspeedJobName = "openstack-lightspeed" + + // OLSConfigName - OLS forbids other name for OLSConfig instance than OLSConfigName + OLSConfigName = "cluster" +) + +// 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") +} + +// IsOLSOperatorInstalled checks whether OLS Operator is already running in the cluster. +func IsOLSOperatorInstalled(ctx context.Context, helper *common_helper.Helper) (bool, error) { + csvGVR := schema.GroupVersionResource{ + Group: "operators.coreos.com", + Version: "v1alpha1", + Resource: "clusterserviceversions", + } + + csvList := &uns.UnstructuredList{} + csvList.SetGroupVersionKind(csvGVR.GroupVersion().WithKind("clusterserviceversion")) + err := helper.GetClient().List(ctx, csvList) + if err != nil { + return false, err + } + + for _, csv := range csvList.Items { + if strings.HasPrefix(csv.GetName(), "lightspeed-operator") { + return true, nil + } + } + + return false, nil +} + +// PatchOLSConfig patches OLSConfig with information from OpenStackLightspeed instance. +func PatchOLSConfig( + helper *common_helper.Helper, + instance *lightspeedv1.OpenStackLightspeed, + olsConfig *uns.Unstructured, + indexID string, +) error { + // 1. 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{}{}, + }, + }, + "name": OpenStackLightspeedDefaultProvider, + "type": instance.Spec.LLMEndpointType, + "url": instance.Spec.LLMEndpoint, + }, + } + if err := uns.SetNestedSlice(olsConfig.Object, providersPatch, "spec", "llm", "providers"); err != nil { + return err + } + + // 2. Patch the RAG section + openstackRAG := []interface{}{ + map[string]interface{}{ + "image": instance.Spec.RAGImage, + "indexID": indexID, + "indexPath": OpenStackLightspeedVectorDBPath, + }, + } + + if err := uns.SetNestedSlice(olsConfig.Object, openstackRAG, "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 + } + + // 3. 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 + } + + // 4. 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 required conditions are true for OLSConfig +func IsOLSConfigReady(ctx context.Context, helper *common_helper.Helper) (bool, error) { + olsConfig, err := GetOLSConfig(ctx, helper) + if err != nil { + return false, err + } + + olsConfigStatusList, found, err := uns.NestedSlice(olsConfig.Object, "status", "conditions") + if !found { + return false, err + } + + jsonData, err := json.Marshal(olsConfigStatusList) + if err != nil { + return false, fmt.Errorf("failed to marshal OLSConfig status: %w", err) + } + + var OLSConfigConditions []metav1.Condition + err = json.Unmarshal(jsonData, &OLSConfigConditions) + if err != nil { + return false, fmt.Errorf("failed to unmarshal JSON containing condition.Conditions: %w", err) + } + + requiredConditionTypes := []string{"ConsolePluginReady", "CacheReady", "ApiReady", "Reconciled"} + for _, OLSConfigCondition := range OLSConfigConditions { + for _, requiredConditionType := range requiredConditionTypes { + if OLSConfigCondition.Type == requiredConditionType && OLSConfigCondition.Status != metav1.ConditionTrue { + return false, nil + } + } + } + + return true, nil +} + +// ResolveIndexID - returns index ID for the data stored in the vector DB container image. The discovery of the +// index ID is done through spawning a pod with the rag-content image and looking at the INDEX_NAME env variable value. +func ResolveIndexID( + ctx context.Context, + helper *common_helper.Helper, + instance *lightspeedv1.OpenStackLightspeed, +) (string, ctrl.Result, error) { + result, err := createOLSJob(ctx, helper, instance) + if err != nil { + return "", result, err + } + + podList := &corev1.PodList{} + labelSelector := client.MatchingLabels{"app": OpenStackLightspeedJobName} + if err := helper.GetClient().List(ctx, podList, client.InNamespace(instance.Namespace), labelSelector); err != nil { + return "", ctrl.Result{}, err + } + + var OLSPod *corev1.Pod + for _, pod := range podList.Items { + if pod.Spec.Containers[0].Image == instance.Spec.RAGImage { + OLSPod = &pod + break + } + } + if OLSPod == nil { + return requeueWaitingPod(helper, instance) + } + + switch OLSPod.Status.Phase { + case corev1.PodSucceeded: + indexName, err := extractEnvFromPodLogs(ctx, OLSPod, "INDEX_NAME") + if err != nil && k8s_errors.IsNotFound(err) { + return requeueWaitingPod(helper, instance) + } + return indexName, ctrl.Result{}, err + case corev1.PodFailed: + return "", ctrl.Result{}, fmt.Errorf("failed to start OpenStack Lightpseed RAG pod") + default: + return requeueWaitingPod(helper, instance) + } +} + +// extractEnvFromPodLogs - discovers an environment variable value from the pod logs. The pod must be started using +// createOLSJob. +func extractEnvFromPodLogs(ctx context.Context, pod *corev1.Pod, envVarName string) (string, error) { + cfg, err := config.GetConfig() + if err != nil { + return "", err + } + + k8sClient, err := kubernetes.NewForConfig(cfg) + if err != nil { + return "", err + } + + req := k8sClient.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &corev1.PodLogOptions{}) + podLogs, err := req.Stream(ctx) + if err != nil { + return "", err + } + defer func() { _ = podLogs.Close() }() + + buf := new(strings.Builder) + _, err = io.Copy(buf, podLogs) + if err != nil { + return "", fmt.Errorf("error in copying logs: %w", err) + } + + logs := buf.String() + for _, envLine := range strings.Split(logs, "\n") { + parts := strings.Split(envLine, "=") + if len(parts) != 2 { + continue + } + + if parts[0] == envVarName { + return parts[1], nil + } + } + + return "", fmt.Errorf("env var not discovered: %s", envVarName) +} + +// createOLSJob - starts OLS pod with entrypoint that lists environment variables after the start of the pod. It used +// to discover INDEX_NAME value. +func createOLSJob( + ctx context.Context, + helper *common_helper.Helper, + instance *lightspeedv1.OpenStackLightspeed, +) (ctrl.Result, error) { + imageHash := sha256.Sum256([]byte(instance.Spec.RAGImage)) + imageHashStr := fmt.Sprintf("%x", imageHash) + imageHashStr = imageHashStr[len(imageHashStr)-9:] + imageName := fmt.Sprintf("%s-%s", OpenStackLightspeedJobName, imageHashStr) + + ttlSecondsAfterFinished := int32(600) // 10 mins + activeDeadlineSeconds := int64(1200) // 20 mins + OLSPod := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: imageName, + Namespace: instance.Namespace, + Labels: map[string]string{ + "app": OpenStackLightspeedJobName, + }, + }, + Spec: batchv1.JobSpec{ + TTLSecondsAfterFinished: &ttlSecondsAfterFinished, + ActiveDeadlineSeconds: &activeDeadlineSeconds, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": OpenStackLightspeedJobName, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "rag-content", + Image: instance.Spec.RAGImage, + Command: []string{"/bin/sh", "-c"}, + Args: []string{"env"}, + }, + }, + RestartPolicy: corev1.RestartPolicyNever, + }, + }, + }, + } + + if err := controllerutil.SetControllerReference(instance, OLSPod, helper.GetScheme()); err != nil { + return ctrl.Result{}, err + } + + err := helper.GetClient().Create(ctx, OLSPod) + if err != nil && !k8s_errors.IsAlreadyExists(err) { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func requeueWaitingPod(helper *common_helper.Helper, instance *lightspeedv1.OpenStackLightspeed) (string, ctrl.Result, error) { + instance.Status.Conditions.Set(condition.FalseCondition( + lightspeedv1.OpenStackLightspeedReadyCondition, + condition.RequestedReason, + condition.SeverityInfo, + lightspeedv1.OpenStackLightspeedWaitingVectorDBMessage, + )) + helper.GetLogger().Info(lightspeedv1.OpenStackLightspeedReadyMessage) + return "", ctrl.Result{RequeueAfter: 5 * time.Second}, nil +} diff --git a/tests/functional/ctlplane/suite_test.go b/tests/functional/ctlplane/suite_test.go index fd2cf9aba..a4399871b 100644 --- a/tests/functional/ctlplane/suite_test.go +++ b/tests/functional/ctlplane/suite_test.go @@ -51,6 +51,7 @@ import ( openstackclientv1 "github.com/openstack-k8s-operators/openstack-operator/apis/client/v1beta1" corev1 "github.com/openstack-k8s-operators/openstack-operator/apis/core/v1beta1" dataplanev1beta1 "github.com/openstack-k8s-operators/openstack-operator/apis/dataplane/v1beta1" + lightspeedv1 "github.com/openstack-k8s-operators/openstack-operator/apis/lightspeed/v1beta1" "github.com/openstack-k8s-operators/openstack-operator/pkg/openstack" ovnv1 "github.com/openstack-k8s-operators/ovn-operator/api/v1beta1" placementv1 "github.com/openstack-k8s-operators/placement-operator/api/v1beta1" @@ -60,6 +61,7 @@ import ( client_ctrl "github.com/openstack-k8s-operators/openstack-operator/controllers/client" core_ctrl "github.com/openstack-k8s-operators/openstack-operator/controllers/core" + lightspeed_ctrl "github.com/openstack-k8s-operators/openstack-operator/controllers/lightspeed" ocp_configv1 "github.com/openshift/api/config/v1" rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" @@ -227,6 +229,8 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) + err = lightspeedv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) err = openstackclientv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) err = corev1.AddToScheme(scheme.Scheme) @@ -341,8 +345,16 @@ var _ = BeforeSuite(func() { core_ctrl.SetupVersionDefaults() openstack.SetupServiceOperatorDefaults() openstackclientv1.SetupDefaults() + lightspeedv1.SetupDefaults() corev1.SetupVersionDefaults() + err = (&lightspeed_ctrl.OpenStackLightspeedReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Kclient: kclient, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + err = (&client_ctrl.OpenStackClientReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(),