Skip to content

Commit 1d35a51

Browse files
Akrogclaude
andcommitted
Add feature flag for rhos-mcps
For now we don't want to automatically deploy the MCP tools, because: - Releasing the rhos-mcps image may be done at a later time. - This is an experimental feature right now, so we want it disabled by default. The name of the feature flag is `rhos_mcps` as shown in the config ample. Jira: OSPRH-27075 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 344b53c commit 1d35a51

26 files changed

Lines changed: 315 additions & 102 deletions

api/v1beta1/conditions.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ const (
5959
// OpenStackLightspeedMCPServerWaitingOpenStack
6060
OpenStackLightspeedMCPServerWaitingOpenStack = "MCP server deployed, waiting for OpenStackControlPlane to become ready"
6161

62+
// OpenStackLightspeedMCPServerDisabledMessage
63+
OpenStackLightspeedMCPServerDisabledMessage = "RHOS MCP server is disabled (rhos_mcps feature flag not set)"
64+
6265
// DeploymentCheckFailedMessage
6366
DeploymentCheckFailedMessage = "Failed to check deployment status: %s"
6467

config/samples/api_v1beta1_openstacklightspeed.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ spec:
2020
# ogxLogLevel: "all=info"
2121
# lightspeedStackLogLevel: "INFO"
2222
# dataverseExporterLogLevel: "INFO"
23-
# Uncomment to enable OKP (Offline Knowledge Portal) as an Inline RAG source:
24-
# okp:
25-
# accessKey: okp-access-key-secret
23+
# Uncomment to enable RHOS MCPs (MCP server sidecar with OpenStack/OpenShift tools)
24+
# and/or OKP (Offline Knowledge Portal) as an Inline RAG source:
2625
# dev:
2726
# featureFlags:
27+
# - rhos_mcps
2828
# - okp
2929
# okpChunkFilterQuery: "product:(*openstack* OR *openshift*)"
3030
# okpRagOnly: true
31+
# okp:
32+
# accessKey: okp-access-key-secret

internal/controller/common.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"encoding/json"
2525
"errors"
2626
"fmt"
27+
"slices"
2728
"strings"
2829

2930
common_cm "github.com/openstack-k8s-operators/lib-common/modules/common/configmap"
@@ -165,6 +166,15 @@ func parseDevConfig(instance *apiv1beta1.OpenStackLightspeed) (apiv1beta1.DevSpe
165166
return config, nil
166167
}
167168

169+
// isRHOSMCPEnabled returns true if the "rhos_mcps" feature flag is present in the dev config.
170+
func isRHOSMCPEnabled(instance *apiv1beta1.OpenStackLightspeed) (bool, error) {
171+
config, err := parseDevConfig(instance)
172+
if err != nil {
173+
return false, err
174+
}
175+
return slices.Contains(config.FeatureFlags, "rhos_mcps"), nil
176+
}
177+
168178
// getOKPChunkFilterQuery returns the chunk filter query from the dev config, or a version-aware default.
169179
func getOKPChunkFilterQuery(ctx context.Context, h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) string {
170180
config, _ := parseDevConfig(instance)

internal/controller/lcore_config.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,17 @@ func buildLCoreMCPServersConfig(openStackReady bool) []interface{} {
246246
return mcpServers
247247
}
248248

249+
func buildLCoreMCPServersConfigIfEnabled(instance *apiv1beta1.OpenStackLightspeed) ([]interface{}, error) {
250+
enabled, err := isRHOSMCPEnabled(instance)
251+
if err != nil {
252+
return nil, fmt.Errorf("failed to parse dev config: %w", err)
253+
}
254+
if !enabled {
255+
return []interface{}{}, nil
256+
}
257+
return buildLCoreMCPServersConfig(instance.Status.OpenStackReady), nil
258+
}
259+
249260
// buildLCoreConfigYAML assembles the complete Lightspeed Core Service configuration and converts to YAML.
250261
// NOTE: quota handlers, and tools approval features are disabled for OpenStack Lightspeed.
251262
func buildLCoreConfigYAML(ctx context.Context, h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) (string, error) {
@@ -255,6 +266,11 @@ func buildLCoreConfigYAML(ctx context.Context, h *common_helper.Helper, instance
255266
"inline": ragInline,
256267
}
257268

269+
mcpServers, err := buildLCoreMCPServersConfigIfEnabled(instance)
270+
if err != nil {
271+
return "", err
272+
}
273+
258274
// Build the complete config as a map
259275
config := map[string]interface{}{
260276
"name": "Lightspeed Core Service (LCS)",
@@ -268,7 +284,7 @@ func buildLCoreConfigYAML(ctx context.Context, h *common_helper.Helper, instance
268284
"conversation_cache": buildLCoreConversationCacheConfig(h, instance),
269285
"byok_rag": []interface{}{},
270286
"rag": ragConfig,
271-
"mcp_servers": buildLCoreMCPServersConfig(instance.Status.OpenStackReady),
287+
"mcp_servers": mcpServers,
272288
}
273289

274290
config["okp"] = buildOKPConfig(ctx, h, instance)

internal/controller/lcore_deployment.go

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -205,26 +205,32 @@ func buildLCorePodTemplateSpec(h *common_helper.Helper, ctx context.Context, ins
205205
containers = append(containers, exporterContainer)
206206
}
207207

208-
// MCP sidecar
209-
mcpMounts := []corev1.VolumeMount{}
210-
addMCPVolumesAndMounts(&volumes, &mcpMounts, instance.Status.OpenStackReady)
211-
212-
mcpContainer := corev1.Container{
213-
Name: "rhos-mcp",
214-
Image: apiv1beta1.OpenStackLightspeedDefaultValues.MCPServerImageURL,
215-
VolumeMounts: mcpMounts,
216-
Resources: corev1.ResourceRequirements{
217-
Requests: corev1.ResourceList{
218-
corev1.ResourceCPU: resource.MustParse("50m"),
219-
corev1.ResourceMemory: resource.MustParse("64Mi"),
220-
},
221-
Limits: corev1.ResourceList{
222-
corev1.ResourceMemory: resource.MustParse("200Mi"),
208+
// MCP sidecar (only when rhos_mcps feature flag is enabled)
209+
rhosMCPEnabled, err := isRHOSMCPEnabled(instance)
210+
if err != nil {
211+
return corev1.PodTemplateSpec{}, fmt.Errorf("failed to parse dev config: %w", err)
212+
}
213+
if rhosMCPEnabled {
214+
mcpMounts := []corev1.VolumeMount{}
215+
addMCPVolumesAndMounts(&volumes, &mcpMounts, instance.Status.OpenStackReady)
216+
217+
mcpContainer := corev1.Container{
218+
Name: "rhos-mcp",
219+
Image: apiv1beta1.OpenStackLightspeedDefaultValues.MCPServerImageURL,
220+
VolumeMounts: mcpMounts,
221+
Resources: corev1.ResourceRequirements{
222+
Requests: corev1.ResourceList{
223+
corev1.ResourceCPU: resource.MustParse("50m"),
224+
corev1.ResourceMemory: resource.MustParse("64Mi"),
225+
},
226+
Limits: corev1.ResourceList{
227+
corev1.ResourceMemory: resource.MustParse("200Mi"),
228+
},
223229
},
224-
},
225-
ImagePullPolicy: corev1.PullIfNotPresent,
230+
ImagePullPolicy: corev1.PullIfNotPresent,
231+
}
232+
containers = append(containers, mcpContainer)
226233
}
227-
containers = append(containers, mcpContainer)
228234

229235
// Build configmap resource version annotations for change detection
230236
annotations, err := buildConfigMapAnnotations(h, ctx)

internal/controller/mcp_server.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"text/template"
2424

25+
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
2526
common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper"
2627
apiv1beta1 "github.com/openstack-k8s-operators/lightspeed-operator/api/v1beta1"
2728
corev1 "k8s.io/api/core/v1"
@@ -114,6 +115,38 @@ func GetMCPServerURL() string {
114115
// Reconciliation
115116
// ---------------------------------------------------------------------------
116117

118+
// ReconcileMCPServerTask reconciles the MCP server as a ReconcileFunc.
119+
func (r *OpenStackLightspeedReconciler) ReconcileMCPServerTask(h *common_helper.Helper, ctx context.Context, instance *apiv1beta1.OpenStackLightspeed) error {
120+
rhosMCPEnabled, err := isRHOSMCPEnabled(instance)
121+
if err != nil {
122+
return fmt.Errorf("failed to parse dev config: %w", err)
123+
}
124+
if rhosMCPEnabled {
125+
openStackReady, mcpErr := r.ReconcileMCPServer(ctx, h, instance)
126+
if mcpErr != nil {
127+
instance.Status.Conditions.Set(condition.FalseCondition(
128+
apiv1beta1.OpenStackLightspeedMCPServerReadyCondition,
129+
condition.ErrorReason,
130+
condition.SeverityWarning,
131+
apiv1beta1.DeploymentCheckFailedMessage,
132+
mcpErr.Error(),
133+
))
134+
return mcpErr
135+
}
136+
instance.Status.OpenStackReady = openStackReady
137+
} else {
138+
if err := r.cleanupMCPResources(ctx, h, instance); err != nil {
139+
return err
140+
}
141+
instance.Status.OpenStackReady = false
142+
instance.Status.Conditions.MarkTrue(
143+
apiv1beta1.OpenStackLightspeedMCPServerReadyCondition,
144+
apiv1beta1.OpenStackLightspeedMCPServerDisabledMessage,
145+
)
146+
}
147+
return nil
148+
}
149+
117150
// ReconcileMCPServer performs the reconciliation of the MCP server.
118151
// The MCP server runs as a sidecar in the LCore pod. The OpenStack MCP tools
119152
// are only configured in lightspeed-stack when OpenStackControlPlane exists and is Ready.
@@ -301,6 +334,52 @@ func extractOSCPFields(
301334
}, nil
302335
}
303336

337+
// ---------------------------------------------------------------------------
338+
// Deletion
339+
// ---------------------------------------------------------------------------
340+
341+
// cleanupMCPResources removes MCP server resources when the rhos_mcps feature
342+
// flag is disabled.
343+
func (r *OpenStackLightspeedReconciler) cleanupMCPResources(
344+
ctx context.Context,
345+
helper *common_helper.Helper,
346+
instance *apiv1beta1.OpenStackLightspeed,
347+
) error {
348+
log := helper.GetLogger()
349+
ns := instance.Namespace
350+
351+
mcpCM := &corev1.ConfigMap{}
352+
mcpCM.Name = MCPConfigYAMLConfigMapName
353+
mcpCM.Namespace = ns
354+
if err := helper.GetClient().Delete(ctx, mcpCM); err != nil && !k8s_errors.IsNotFound(err) {
355+
return fmt.Errorf("failed to delete MCP config ConfigMap: %w", err)
356+
}
357+
358+
cloudsCM := &corev1.ConfigMap{}
359+
cloudsCM.Name = CloudsYAMLConfigMapName
360+
cloudsCM.Namespace = ns
361+
if err := helper.GetClient().Delete(ctx, cloudsCM); err != nil && !k8s_errors.IsNotFound(err) {
362+
return fmt.Errorf("failed to delete openstack-config ConfigMap: %w", err)
363+
}
364+
365+
secureSec := &corev1.Secret{}
366+
secureSec.Name = SecureYAMLSecretName
367+
secureSec.Namespace = ns
368+
if err := helper.GetClient().Delete(ctx, secureSec); err != nil && !k8s_errors.IsNotFound(err) {
369+
return fmt.Errorf("failed to delete openstack-config-secret Secret: %w", err)
370+
}
371+
372+
caSec := &corev1.Secret{}
373+
caSec.Name = CombinedCABundleSecretName
374+
caSec.Namespace = ns
375+
if err := helper.GetClient().Delete(ctx, caSec); err != nil && !k8s_errors.IsNotFound(err) {
376+
return fmt.Errorf("failed to delete combined-ca-bundle Secret: %w", err)
377+
}
378+
379+
log.Info("RHOS MCP resources cleaned up")
380+
return nil
381+
}
382+
304383
// copyObjectsToOpenStackLightspeedNamespace copies the required ConfigMaps and Secrets
305384
// from the OpenStackControlPlane's namespace to the OpenStack Lightspeed namespace.
306385
func copyObjectsToOpenStackLightspeedNamespace(

internal/controller/openstacklightspeed_controller.go

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -205,24 +205,8 @@ func (r *OpenStackLightspeedReconciler) Reconcile(ctx context.Context, req ctrl.
205205
Log.Error(err, "failed to parse dev config, ignoring")
206206
}
207207

208-
// Reconcile MCP server before LCore resources, because its result
209-
// determines what goes into the lightspeed-stack config (mcp_servers section).
210-
openStackReady, mcpErr := r.ReconcileMCPServer(ctx, helper, instance)
211-
if mcpErr != nil {
212-
instance.Status.Conditions.Set(condition.FalseCondition(
213-
apiv1beta1.OpenStackLightspeedMCPServerReadyCondition,
214-
condition.ErrorReason,
215-
condition.SeverityWarning,
216-
apiv1beta1.DeploymentCheckFailedMessage,
217-
mcpErr.Error(),
218-
))
219-
return ctrl.Result{}, mcpErr
220-
}
221-
222-
// Store the OpenStack readiness for config generation
223-
instance.Status.OpenStackReady = openStackReady
224-
225208
reconcileTasks := []ReconcileTask{
209+
{Name: "MCPServer", Task: r.ReconcileMCPServerTask},
226210
{Name: "PostgresResources", Task: ReconcilePostgresResources},
227211
{Name: "PostgresDeployment", Task: ReconcilePostgresDeployment},
228212
{Name: "OKPDeployment", Task: ReconcileOKPDeployment},
@@ -309,17 +293,24 @@ func (r *OpenStackLightspeedReconciler) reconcileStatus(
309293
}
310294
}
311295

312-
// Mark MCP server condition based on readiness
313-
if instance.Status.OpenStackReady {
314-
instance.Status.Conditions.MarkTrue(
315-
apiv1beta1.OpenStackLightspeedMCPServerReadyCondition,
316-
apiv1beta1.OpenStackLightspeedMCPServerDeployed,
317-
)
318-
} else {
319-
instance.Status.Conditions.MarkTrue(
320-
apiv1beta1.OpenStackLightspeedMCPServerReadyCondition,
321-
apiv1beta1.OpenStackLightspeedMCPServerWaitingOpenStack,
322-
)
296+
// Mark MCP server condition based on readiness (only when RHOS MCP is enabled;
297+
// when disabled the condition was already set in Reconcile).
298+
rhosMCPEnabled, err := isRHOSMCPEnabled(instance)
299+
if err != nil {
300+
return ctrl.Result{}, fmt.Errorf("failed to parse dev config: %w", err)
301+
}
302+
if rhosMCPEnabled {
303+
if instance.Status.OpenStackReady {
304+
instance.Status.Conditions.MarkTrue(
305+
apiv1beta1.OpenStackLightspeedMCPServerReadyCondition,
306+
apiv1beta1.OpenStackLightspeedMCPServerDeployed,
307+
)
308+
} else {
309+
instance.Status.Conditions.MarkTrue(
310+
apiv1beta1.OpenStackLightspeedMCPServerReadyCondition,
311+
apiv1beta1.OpenStackLightspeedMCPServerWaitingOpenStack,
312+
)
313+
}
323314
}
324315

325316
instance.Status.Conditions.MarkTrue(

test/kuttl/common/expected-configs/lightspeed-stack-update.yaml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,7 @@ inference:
7171
llama_stack:
7272
url: http://localhost:8321
7373
use_as_library_client: false
74-
mcp_servers:
75-
- authorization_headers:
76-
OCP_TOKEN: kubernetes
77-
name: rhos-ocp-tools
78-
url: http://127.0.0.1:8080/openshift/
74+
mcp_servers: []
7975
name: Lightspeed Core Service (LCS)
8076
okp:
8177
chunk_filter_query: product:(*openstack* OR *openshift*)

test/kuttl/common/expected-configs/lightspeed-stack.yaml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,7 @@ inference:
7171
llama_stack:
7272
url: http://localhost:8321
7373
use_as_library_client: false
74-
mcp_servers:
75-
- authorization_headers:
76-
OCP_TOKEN: kubernetes
77-
name: rhos-ocp-tools
78-
url: http://127.0.0.1:8080/openshift/
74+
mcp_servers: []
7975
name: Lightspeed Core Service (LCS)
8076
okp:
8177
chunk_filter_query: product:(*openstack* OR *openshift*)

test/kuttl/common/openstack-lightspeed-instance/assert-openstack-lightspeed-instance.yaml

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,6 @@ spec:
311311
mountPath: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem
312312
subPath: tls-ca-bundle.pem
313313
readOnly: true
314-
- name: rhos-mcp
315314
volumes:
316315
- name: ogx-config
317316
configMap:
@@ -337,12 +336,6 @@ spec:
337336
- name: tls-certs
338337
secret:
339338
secretName: lightspeed-tls
340-
- name: mcp-config
341-
configMap:
342-
name: mcp-config
343-
items:
344-
- key: config.yaml
345-
path: config.yaml
346339
status:
347340
replicas: 1
348341
readyReplicas: 1
@@ -362,14 +355,6 @@ metadata:
362355
name: vector-db-scripts
363356
namespace: openstack-lightspeed
364357

365-
# MCP server resources
366-
---
367-
apiVersion: v1
368-
kind: ConfigMap
369-
metadata:
370-
name: mcp-config
371-
namespace: openstack-lightspeed
372-
373358
# Console Plugin resources
374359
---
375360
apiVersion: v1
@@ -467,6 +452,7 @@ status:
467452
message: Setup complete
468453
- type: OpenStackLightspeedMCPServerReady
469454
status: "True"
455+
message: "RHOS MCP server is disabled (rhos_mcps feature flag not set)"
470456
- type: OpenStackLightspeedReady
471457
status: "True"
472458
reason: Ready

0 commit comments

Comments
 (0)