444 lines
15 KiB
Go
444 lines
15 KiB
Go
/*
|
|
Copyright 2026.
|
|
|
|
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 controller
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
|
|
|
lmcachev1alpha1 "github.com/LMCache/LMCache/api/v1alpha1"
|
|
"github.com/LMCache/LMCache/internal/resources"
|
|
)
|
|
|
|
// validateAndSetCondition runs validation and updates the ConfigValid condition.
|
|
// Returns an error if validation fails (to stop reconciliation).
|
|
func (r *CacheBlendEngineReconciler) validateAndSetCondition(ctx context.Context, engine *lmcachev1alpha1.CacheBlendEngine) error {
|
|
errs := engine.ValidateSpec()
|
|
|
|
if len(errs) > 0 {
|
|
// Re-fetch to get the latest resourceVersion before status update.
|
|
generation := engine.Generation
|
|
if err := r.Get(ctx, types.NamespacedName{Name: engine.Name, Namespace: engine.Namespace}, engine); err != nil {
|
|
return fmt.Errorf("failed to re-fetch engine for status update: %w", err)
|
|
}
|
|
meta.SetStatusCondition(&engine.Status.Conditions, metav1.Condition{
|
|
Type: lmcachev1alpha1.ConditionConfigValid,
|
|
Status: metav1.ConditionFalse,
|
|
Reason: "ValidationFailed",
|
|
Message: errs.ToAggregate().Error(),
|
|
ObservedGeneration: generation,
|
|
})
|
|
engine.Status.Phase = lmcachev1alpha1.PhaseFailed
|
|
engine.Status.ObservedGeneration = generation
|
|
if err := r.Status().Update(ctx, engine); err != nil {
|
|
return fmt.Errorf("failed to update status after validation failure: %w", err)
|
|
}
|
|
return fmt.Errorf("spec validation failed: %s", errs.ToAggregate().Error())
|
|
}
|
|
|
|
// ConfigValid=True condition is set in updateStatus (after re-fetch)
|
|
// to avoid resourceVersion conflicts.
|
|
return nil
|
|
}
|
|
|
|
// reconcileDaemonSet creates or updates the blend_v3 engine DaemonSet.
|
|
func (r *CacheBlendEngineReconciler) reconcileDaemonSet(ctx context.Context, engine *lmcachev1alpha1.CacheBlendEngine) error {
|
|
// Resolve the coordinator connection (ref -> Service URL) so the DaemonSet
|
|
// builder emits --coordinator-url for server registration.
|
|
conn, err := resources.ResolveCoordinatorConnection(ctx, r.Client, engine.Namespace, engine.Spec.Coordinator)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
engine.Spec.Coordinator = conn
|
|
|
|
desired := resources.BuildCBEngineDaemonSet(engine)
|
|
|
|
existing := &appsv1.DaemonSet{}
|
|
err = r.Get(ctx, types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, existing)
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
if err := ctrl.SetControllerReference(engine, desired, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
return r.Create(ctx, desired)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Preserve immutable selector
|
|
desired.Spec.Selector = existing.Spec.Selector
|
|
desired.Spec.Template.Labels = resources.MergeLabels(
|
|
existing.Spec.Selector.MatchLabels,
|
|
desired.Spec.Template.Labels,
|
|
)
|
|
|
|
if err := ctrl.SetControllerReference(engine, desired, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
|
|
patch := client.MergeFrom(existing.DeepCopy())
|
|
existing.Spec.Template = desired.Spec.Template
|
|
existing.Labels = desired.Labels
|
|
|
|
return r.Patch(ctx, existing, patch)
|
|
}
|
|
|
|
// reconcileLookupService creates or updates the node-local lookup Service.
|
|
func (r *CacheBlendEngineReconciler) reconcileLookupService(ctx context.Context, engine *lmcachev1alpha1.CacheBlendEngine) error {
|
|
desired := resources.BuildCBEngineLookupService(engine)
|
|
|
|
existing := &corev1.Service{}
|
|
err := r.Get(ctx, types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, existing)
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
if err := ctrl.SetControllerReference(engine, desired, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
return r.Create(ctx, desired)
|
|
}
|
|
return err
|
|
}
|
|
|
|
if err := ctrl.SetControllerReference(engine, desired, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
|
|
patch := client.MergeFrom(existing.DeepCopy())
|
|
existing.Spec.Ports = desired.Spec.Ports
|
|
existing.Spec.InternalTrafficPolicy = desired.Spec.InternalTrafficPolicy
|
|
existing.Labels = desired.Labels
|
|
|
|
return r.Patch(ctx, existing, patch)
|
|
}
|
|
|
|
// reconcileMetricsService creates or updates the headless metrics Service.
|
|
func (r *CacheBlendEngineReconciler) reconcileMetricsService(ctx context.Context, engine *lmcachev1alpha1.CacheBlendEngine) error {
|
|
desired := resources.BuildCBEngineMetricsService(engine)
|
|
|
|
existing := &corev1.Service{}
|
|
err := r.Get(ctx, types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, existing)
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
if err := ctrl.SetControllerReference(engine, desired, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
return r.Create(ctx, desired)
|
|
}
|
|
return err
|
|
}
|
|
|
|
if err := ctrl.SetControllerReference(engine, desired, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
|
|
patch := client.MergeFrom(existing.DeepCopy())
|
|
existing.Spec.Ports = desired.Spec.Ports
|
|
existing.Labels = desired.Labels
|
|
|
|
return r.Patch(ctx, existing, patch)
|
|
}
|
|
|
|
// reconcileConnectionConfigMap creates or updates the <engine>-connection ConfigMap.
|
|
func (r *CacheBlendEngineReconciler) reconcileConnectionConfigMap(ctx context.Context, engine *lmcachev1alpha1.CacheBlendEngine) error {
|
|
desired := resources.BuildCBConnectionConfigMap(engine)
|
|
|
|
existing := &corev1.ConfigMap{}
|
|
err := r.Get(ctx, types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, existing)
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
if err := ctrl.SetControllerReference(engine, desired, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
return r.Create(ctx, desired)
|
|
}
|
|
return err
|
|
}
|
|
|
|
if err := ctrl.SetControllerReference(engine, desired, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
|
|
patch := client.MergeFrom(existing.DeepCopy())
|
|
existing.Data = desired.Data
|
|
existing.Labels = desired.Labels
|
|
|
|
return r.Patch(ctx, existing, patch)
|
|
}
|
|
|
|
// reconcileServiceMonitor creates, updates, or deletes the ServiceMonitor.
|
|
func (r *CacheBlendEngineReconciler) reconcileServiceMonitor(ctx context.Context, engine *lmcachev1alpha1.CacheBlendEngine) error {
|
|
log := logf.FromContext(ctx)
|
|
|
|
if !resources.CBServiceMonitorEnabled(engine) {
|
|
// Delete ServiceMonitor if it exists
|
|
existing := &monitoringv1.ServiceMonitor{}
|
|
err := r.Get(ctx, types.NamespacedName{Name: engine.Name, Namespace: engine.Namespace}, existing)
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
// If the CRD is not installed, ignore the error
|
|
if meta.IsNoMatchError(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
log.Info("Deleting ServiceMonitor", "name", engine.Name)
|
|
return r.Delete(ctx, existing)
|
|
}
|
|
|
|
desired := resources.BuildCBServiceMonitor(engine)
|
|
|
|
existing := &monitoringv1.ServiceMonitor{}
|
|
err := r.Get(ctx, types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, existing)
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
if err := ctrl.SetControllerReference(engine, desired, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
return r.Create(ctx, desired)
|
|
}
|
|
if meta.IsNoMatchError(err) {
|
|
log.Info("ServiceMonitor CRD not available, skipping")
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
if err := ctrl.SetControllerReference(engine, desired, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
|
|
patch := client.MergeFrom(existing.DeepCopy())
|
|
existing.Spec = desired.Spec
|
|
existing.Labels = desired.Labels
|
|
|
|
return r.Patch(ctx, existing, patch)
|
|
}
|
|
|
|
// updateStatus queries the DaemonSet and pods to compute status fields.
|
|
// It re-fetches the engine to get the latest resourceVersion, avoiding
|
|
// conflicts from watch events triggered by earlier reconcile steps.
|
|
func (r *CacheBlendEngineReconciler) updateStatus(ctx context.Context, engine *lmcachev1alpha1.CacheBlendEngine) error {
|
|
// Re-fetch to get the latest resourceVersion, avoiding conflicts
|
|
// from watch events triggered by earlier reconcile steps (e.g.
|
|
// DaemonSet/Service creation fires Owns watches).
|
|
if err := r.Get(ctx, types.NamespacedName{Name: engine.Name, Namespace: engine.Namespace}, engine); err != nil {
|
|
return err
|
|
}
|
|
|
|
// ConfigValid condition (set here after re-fetch so it's not lost).
|
|
meta.SetStatusCondition(&engine.Status.Conditions, metav1.Condition{
|
|
Type: lmcachev1alpha1.ConditionConfigValid,
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "Valid",
|
|
Message: "Spec validation passed",
|
|
ObservedGeneration: engine.Generation,
|
|
})
|
|
|
|
ds := &appsv1.DaemonSet{}
|
|
err := r.Get(ctx, types.NamespacedName{Name: engine.Name, Namespace: engine.Namespace}, ds)
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
engine.Status.Phase = lmcachev1alpha1.PhasePending
|
|
engine.Status.DesiredInstances = 0
|
|
engine.Status.ReadyInstances = 0
|
|
engine.Status.ObservedGeneration = engine.Generation
|
|
return r.Status().Update(ctx, engine)
|
|
}
|
|
return err
|
|
}
|
|
|
|
engine.Status.DesiredInstances = ds.Status.DesiredNumberScheduled
|
|
engine.Status.ReadyInstances = ds.Status.NumberReady
|
|
engine.Status.ObservedGeneration = engine.Generation
|
|
|
|
// Compute phase
|
|
switch {
|
|
case ds.Status.DesiredNumberScheduled == 0:
|
|
engine.Status.Phase = lmcachev1alpha1.PhasePending
|
|
case ds.Status.NumberReady == ds.Status.DesiredNumberScheduled:
|
|
engine.Status.Phase = lmcachev1alpha1.PhaseRunning
|
|
case ds.Status.NumberReady > 0:
|
|
engine.Status.Phase = lmcachev1alpha1.PhaseDegraded
|
|
default:
|
|
engine.Status.Phase = lmcachev1alpha1.PhasePending
|
|
}
|
|
|
|
// Set conditions
|
|
meta.SetStatusCondition(&engine.Status.Conditions, metav1.Condition{
|
|
Type: lmcachev1alpha1.ConditionAvailable,
|
|
Status: conditionBool(ds.Status.NumberReady > 0),
|
|
Reason: reasonFromReady(ds.Status.NumberReady > 0, "AtLeastOneReady", "NoReadyInstances"),
|
|
Message: fmt.Sprintf("%d/%d instances ready", ds.Status.NumberReady, ds.Status.DesiredNumberScheduled),
|
|
ObservedGeneration: engine.Generation,
|
|
})
|
|
|
|
allReady := ds.Status.NumberReady == ds.Status.DesiredNumberScheduled && ds.Status.DesiredNumberScheduled > 0
|
|
meta.SetStatusCondition(&engine.Status.Conditions, metav1.Condition{
|
|
Type: lmcachev1alpha1.ConditionAllInstancesReady,
|
|
Status: conditionBool(allReady),
|
|
Reason: reasonFromReady(allReady, "AllReady", "NotAllReady"),
|
|
Message: fmt.Sprintf("%d/%d instances ready", ds.Status.NumberReady, ds.Status.DesiredNumberScheduled),
|
|
ObservedGeneration: engine.Generation,
|
|
})
|
|
|
|
// Build endpoints from pods
|
|
serverPort := int32(5555)
|
|
if engine.Spec.Server != nil && engine.Spec.Server.Port != nil {
|
|
serverPort = *engine.Spec.Server.Port
|
|
}
|
|
metricsPort := int32(9090)
|
|
if engine.Spec.Prometheus != nil && engine.Spec.Prometheus.Port != nil {
|
|
metricsPort = *engine.Spec.Prometheus.Port
|
|
}
|
|
|
|
podList := &corev1.PodList{}
|
|
if err := r.List(ctx, podList,
|
|
client.InNamespace(engine.Namespace),
|
|
client.MatchingLabels(resources.SelectorLabels(engine.Name)),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
endpoints := make([]lmcachev1alpha1.EndpointStatus, 0, len(podList.Items))
|
|
for i := range podList.Items {
|
|
pod := &podList.Items[i]
|
|
ready := false
|
|
for _, cond := range pod.Status.Conditions {
|
|
if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue {
|
|
ready = true
|
|
break
|
|
}
|
|
}
|
|
endpoints = append(endpoints, lmcachev1alpha1.EndpointStatus{
|
|
NodeName: pod.Spec.NodeName,
|
|
HostIP: pod.Status.HostIP,
|
|
PodName: pod.Name,
|
|
Port: serverPort,
|
|
MetricsPort: metricsPort,
|
|
Ready: ready,
|
|
})
|
|
}
|
|
engine.Status.Endpoints = endpoints
|
|
|
|
return r.Status().Update(ctx, engine)
|
|
}
|
|
|
|
// reconcileRESPAuthSecret ensures a local copy of the RESP auth secret exists in
|
|
// the engine's namespace. It mirrors the LMCacheEngine path: a no-op unless
|
|
// spec.l2Backend.RESP.authSecretRef is set, otherwise it reads the source secret
|
|
// (possibly cross-namespace) and creates/updates a managed copy owned by the CR.
|
|
func (r *CacheBlendEngineReconciler) reconcileRESPAuthSecret(ctx context.Context, engine *lmcachev1alpha1.CacheBlendEngine) error {
|
|
log := logf.FromContext(ctx)
|
|
spec := &engine.Spec
|
|
|
|
// No RESP auth configured — clean up any stale managed secret.
|
|
if spec.L2Backend == nil || spec.L2Backend.RESP == nil || spec.L2Backend.RESP.AuthSecretRef == nil {
|
|
return r.deleteRESPAuthSecretIfExists(ctx, engine)
|
|
}
|
|
|
|
ref := spec.L2Backend.RESP.AuthSecretRef
|
|
sourceNS := ref.Namespace
|
|
if sourceNS == "" {
|
|
sourceNS = engine.Namespace
|
|
}
|
|
localName := resources.RESPAuthSecretName(engine.Name)
|
|
|
|
// Read the source secret.
|
|
source := &corev1.Secret{}
|
|
if err := r.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: sourceNS}, source); err != nil {
|
|
return fmt.Errorf("failed to read RESP auth secret %s/%s: %w", sourceNS, ref.Name, err)
|
|
}
|
|
|
|
// Validate that the source secret contains the required "password" key.
|
|
password, ok := source.Data["password"]
|
|
if !ok || len(password) == 0 {
|
|
return fmt.Errorf("RESP auth secret %s/%s is missing required 'password' key", sourceNS, ref.Name)
|
|
}
|
|
|
|
// Build the local managed copy.
|
|
// Only "password" is required; "username" is optional (Redis Enterprise
|
|
// often uses password-only auth).
|
|
secretData := map[string][]byte{
|
|
"password": password,
|
|
}
|
|
if u, ok := source.Data["username"]; ok {
|
|
secretData["username"] = u
|
|
}
|
|
desired := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: localName,
|
|
Namespace: engine.Namespace,
|
|
Labels: resources.StandardLabels(engine.Name),
|
|
},
|
|
Data: secretData,
|
|
}
|
|
|
|
existing := &corev1.Secret{}
|
|
err := r.Get(ctx, types.NamespacedName{Name: localName, Namespace: engine.Namespace}, existing)
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
if err := ctrl.SetControllerReference(engine, desired, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
log.Info("Creating managed RESP auth secret", "name", localName, "source", sourceNS+"/"+ref.Name)
|
|
return r.Create(ctx, desired)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Update existing — apply ownerRef, data, and labels.
|
|
if err := ctrl.SetControllerReference(engine, existing, r.Scheme); err != nil {
|
|
return err
|
|
}
|
|
patch := client.MergeFrom(existing.DeepCopy())
|
|
existing.Data = desired.Data
|
|
existing.Labels = desired.Labels
|
|
return r.Patch(ctx, existing, patch)
|
|
}
|
|
|
|
// deleteRESPAuthSecretIfExists removes the managed RESP auth secret if it exists
|
|
// (e.g. when authSecretRef is removed from the spec).
|
|
func (r *CacheBlendEngineReconciler) deleteRESPAuthSecretIfExists(ctx context.Context, engine *lmcachev1alpha1.CacheBlendEngine) error {
|
|
secret := &corev1.Secret{}
|
|
name := resources.RESPAuthSecretName(engine.Name)
|
|
err := r.Get(ctx, types.NamespacedName{Name: name, Namespace: engine.Namespace}, secret)
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
// Only delete if we own it.
|
|
if metav1.IsControlledBy(secret, engine) {
|
|
return r.Delete(ctx, secret)
|
|
}
|
|
return nil
|
|
}
|