From a98d5d558f5f61ffda5c6a53b259037194596cd6 Mon Sep 17 00:00:00 2001 From: Johnny Bouder Date: Fri, 14 Aug 2026 18:45:24 -0400 Subject: [PATCH] fix: stop wiping API-key metadata ConfigMap on LLMModel reconcile The LLMModel reconciler's createOrUpdateConfigMap overwrites existing.Data with the desired data on every reconcile. The desired -api-key-metadata ConfigMap the operator builds is an empty placeholder (the key-manager fills it in), so each reconcile reset the metadata to empty and API keys vanished from the UI while the keys in the -api-keys Secret kept authenticating. Reconcile the metadata ConfigMap with a data-preserving variant that only manages labels, mirroring createOrUpdateSecret (and the PassthroughModel reconciler, which already preserves ConfigMap data). The EPP ConfigMap keeps the overwriting behavior since its data is operator-owned. Fixes #85 Co-Authored-By: Claude Fable 5 --- .../controller/llmmodel_controller.go | 18 ++++++++++- .../controller/llmmodel_controller_test.go | 32 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/operator/internal/controller/llmmodel_controller.go b/operator/internal/controller/llmmodel_controller.go index 00442b8..712aa64 100644 --- a/operator/internal/controller/llmmodel_controller.go +++ b/operator/internal/controller/llmmodel_controller.go @@ -286,7 +286,7 @@ func (r *LLMModelReconciler) reconcileAuthSecretAndConfigMap( if err := r.createOrUpdateSecret(ctx, auth.APIKeySecret); err != nil { return fmt.Errorf("reconciling api key secret: %w", err) } - if err := r.createOrUpdateConfigMap(ctx, auth.APIKeyMetadataCM); err != nil { + if err := r.createOrUpdateConfigMapPreserveData(ctx, auth.APIKeyMetadataCM); err != nil { return fmt.Errorf("reconciling api key metadata configmap: %w", err) } return nil @@ -641,6 +641,22 @@ func (r *LLMModelReconciler) createOrUpdateConfigMap(ctx context.Context, cm *co return r.Update(ctx, existing) } +// createOrUpdateConfigMapPreserveData is for ConfigMaps whose data is owned by +// another writer (the key-manager stores API-key metadata in them); the +// operator only manages labels, mirroring createOrUpdateSecret. +func (r *LLMModelReconciler) createOrUpdateConfigMapPreserveData(ctx context.Context, cm *corev1.ConfigMap) error { + existing := &corev1.ConfigMap{} + err := r.Get(ctx, types.NamespacedName{Name: cm.Name, Namespace: cm.Namespace}, existing) + if apierrors.IsNotFound(err) { + return r.Create(ctx, cm) + } + if err != nil { + return err + } + existing.Labels = cm.Labels + return r.Update(ctx, existing) +} + func (r *LLMModelReconciler) createOrUpdateNetworkPolicy(ctx context.Context, np *networkingv1.NetworkPolicy) error { existing := &networkingv1.NetworkPolicy{} err := r.Get(ctx, types.NamespacedName{Name: np.Name, Namespace: np.Namespace}, existing) diff --git a/operator/internal/controller/llmmodel_controller_test.go b/operator/internal/controller/llmmodel_controller_test.go index 12fb99d..32dc56a 100644 --- a/operator/internal/controller/llmmodel_controller_test.go +++ b/operator/internal/controller/llmmodel_controller_test.go @@ -275,6 +275,38 @@ var _ = Describe("LLMModel Controller", func() { Namespace: testNamespace, }, cm)).To(Succeed()) }) + + It("preserves key data written by the key-manager across reconciles", func() { + By("creating the LLMModel and reconciling") + model := newMinimalModel(modelName) + Expect(k8sClient.Create(ctx, model)).To(Succeed()) + r := newReconciler() + Expect(reconcileUntilStable(r, ctx, reconcileRequest(modelName))).To(Succeed()) + + By("simulating the key-manager writing a key + metadata") + secret := &corev1.Secret{} + secretKey := types.NamespacedName{Name: modelName + "-api-keys", Namespace: testNamespace} + Expect(k8sClient.Get(ctx, secretKey, secret)).To(Succeed()) + secret.Data = map[string][]byte{"key-abc123": []byte("sk-secret-value")} + Expect(k8sClient.Update(ctx, secret)).To(Succeed()) + + cm := &corev1.ConfigMap{} + cmKey := types.NamespacedName{Name: modelName + "-api-key-metadata", Namespace: testNamespace} + Expect(k8sClient.Get(ctx, cmKey, cm)).To(Succeed()) + cm.Data = map[string]string{"key-abc123": `{"name":"my key","created_by":"user"}`} + Expect(k8sClient.Update(ctx, cm)).To(Succeed()) + + By("reconciling again") + Expect(reconcileUntilStable(r, ctx, reconcileRequest(modelName))).To(Succeed()) + + By("verifying the Secret data survived the reconcile") + Expect(k8sClient.Get(ctx, secretKey, secret)).To(Succeed()) + Expect(secret.Data).To(HaveKey("key-abc123")) + + By("verifying the metadata ConfigMap data survived the reconcile") + Expect(k8sClient.Get(ctx, cmKey, cm)).To(Succeed()) + Expect(cm.Data).To(HaveKey("key-abc123")) + }) }) Describe("Deletion with finalizer cleanup", func() {