Skip to content

Commit d7551a2

Browse files
Akrogclaude
andcommitted
MCP: Add feature flag for rhoso-mcps
For now we don't want to automatically deploy the MCP tools, because: - Releasing the lightspeed-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 `rhoso_mcps` as shown in the config sample. Jira: OSPRH-27075 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent da8e161 commit d7551a2

26 files changed

Lines changed: 308 additions & 173 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 = "RHOSO MCP server is disabled (rhoso_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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ 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 RHOSO MCPs (MCP server sidecar with OpenStack/OpenShift tools)
2624
# dev:
2725
# featureFlags:
28-
# - okp
26+
# - rhoso_mcps
2927
# okpChunkFilterQuery: "product:(*openstack* OR *openshift*)"
3028
# okpRagOnly: true
29+
# okp:
30+
# 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
"errors"
2525
"fmt"
2626
"math/big"
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+
// isRHOSOMCPEnabled returns true if the "rhoso_mcps" feature flag is present in the dev config.
170+
func isRHOSOMCPEnabled(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, "rhoso_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 := isRHOSOMCPEnabled(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
@@ -210,26 +210,32 @@ func buildLCorePodTemplateSpec(h *common_helper.Helper, ctx context.Context, ins
210210
containers = append(containers, exporterContainer)
211211
}
212212

213-
// MCP sidecar
214-
mcpMounts := []corev1.VolumeMount{}
215-
addMCPVolumesAndMounts(&volumes, &mcpMounts)
216-
217-
mcpContainer := corev1.Container{
218-
Name: "rhoso-mcps",
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"),
213+
// MCP sidecar (only when rhoso_mcps feature flag is enabled)
214+
rhosoMCPEnabled, err := isRHOSOMCPEnabled(instance)
215+
if err != nil {
216+
return corev1.PodTemplateSpec{}, fmt.Errorf("failed to parse dev config: %w", err)
217+
}
218+
if rhosoMCPEnabled {
219+
mcpMounts := []corev1.VolumeMount{}
220+
addMCPVolumesAndMounts(&volumes, &mcpMounts)
221+
222+
mcpContainer := corev1.Container{
223+
Name: "rhoso-mcps",
224+
Image: apiv1beta1.OpenStackLightspeedDefaultValues.MCPServerImageURL,
225+
VolumeMounts: mcpMounts,
226+
Resources: corev1.ResourceRequirements{
227+
Requests: corev1.ResourceList{
228+
corev1.ResourceCPU: resource.MustParse("50m"),
229+
corev1.ResourceMemory: resource.MustParse("64Mi"),
230+
},
231+
Limits: corev1.ResourceList{
232+
corev1.ResourceMemory: resource.MustParse("200Mi"),
233+
},
228234
},
229-
},
230-
ImagePullPolicy: corev1.PullIfNotPresent,
235+
ImagePullPolicy: corev1.PullIfNotPresent,
236+
}
237+
containers = append(containers, mcpContainer)
231238
}
232-
containers = append(containers, mcpContainer)
233239

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

internal/controller/mcp_server.go

Lines changed: 63 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+
rhosoMCPEnabled, err := isRHOSOMCPEnabled(instance)
121+
if err != nil {
122+
return fmt.Errorf("failed to parse dev config: %w", err)
123+
}
124+
if rhosoMCPEnabled {
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,36 @@ func extractOSCPFields(
301334
}, nil
302335
}
303336

337+
// ---------------------------------------------------------------------------
338+
// Deletion
339+
// ---------------------------------------------------------------------------
340+
341+
// cleanupMCPResources removes MCP server resources when the rhoso_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+
ns := instance.Namespace
349+
350+
resources := []client.Object{
351+
&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: MCPConfigYAMLConfigMapName, Namespace: ns}},
352+
&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: CloudsYAMLConfigMapName, Namespace: ns}},
353+
&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: SecureYAMLSecretName, Namespace: ns}},
354+
&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: CombinedCABundleSecretName, Namespace: ns}},
355+
}
356+
357+
for _, obj := range resources {
358+
if err := helper.GetClient().Delete(ctx, obj); err != nil && !k8s_errors.IsNotFound(err) {
359+
return fmt.Errorf("failed to delete %s %s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err)
360+
}
361+
}
362+
363+
helper.GetLogger().Info("RHOSO MCP resources cleaned up")
364+
return nil
365+
}
366+
304367
// copyObjectsToOpenStackLightspeedNamespace copies the required ConfigMaps and Secrets
305368
// from the OpenStackControlPlane's namespace to the OpenStack Lightspeed namespace.
306369
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 RHOSO MCP is enabled;
297+
// when disabled the condition was already set in Reconcile).
298+
rhosoMCPEnabled, err := isRHOSOMCPEnabled(instance)
299+
if err != nil {
300+
return ctrl.Result{}, fmt.Errorf("failed to parse dev config: %w", err)
301+
}
302+
if rhosoMCPEnabled {
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: rhoso-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: rhoso-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 & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -314,21 +314,6 @@ spec:
314314
mountPath: /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem
315315
subPath: tls-ca-bundle.pem
316316
readOnly: true
317-
- name: rhoso-mcps
318-
volumeMounts:
319-
- name: openstack-config-secret
320-
mountPath: /app/secure.yaml
321-
subPath: secure.yaml
322-
- name: openstack-config
323-
mountPath: /app/clouds.yaml
324-
subPath: clouds.yaml
325-
- name: combined-ca-bundle
326-
mountPath: /app/tls-ca-bundle.pem
327-
subPath: tls-ca-bundle.pem
328-
readOnly: true
329-
- name: mcp-config
330-
mountPath: /app/config.yaml
331-
subPath: config.yaml
332317
volumes:
333318
- name: ogx-config
334319
configMap:
@@ -354,33 +339,6 @@ spec:
354339
- name: tls-certs
355340
secret:
356341
secretName: lightspeed-tls
357-
- name: openstack-config-secret
358-
secret:
359-
secretName: openstack-config-secret
360-
items:
361-
- key: secure.yaml
362-
path: secure.yaml
363-
optional: true
364-
- name: openstack-config
365-
configMap:
366-
name: openstack-config
367-
items:
368-
- key: clouds.yaml
369-
path: clouds.yaml
370-
optional: true
371-
- name: combined-ca-bundle
372-
secret:
373-
secretName: combined-ca-bundle
374-
items:
375-
- key: tls-ca-bundle.pem
376-
path: tls-ca-bundle.pem
377-
optional: true
378-
- name: mcp-config
379-
configMap:
380-
name: mcp-config
381-
items:
382-
- key: config.yaml
383-
path: config.yaml
384342
status:
385343
replicas: 1
386344
readyReplicas: 1
@@ -400,14 +358,6 @@ metadata:
400358
name: vector-db-scripts
401359
namespace: openstack-lightspeed
402360

403-
# MCP server resources
404-
---
405-
apiVersion: v1
406-
kind: ConfigMap
407-
metadata:
408-
name: mcp-config
409-
namespace: openstack-lightspeed
410-
411361
# Console Plugin resources
412362
---
413363
apiVersion: v1
@@ -505,6 +455,7 @@ status:
505455
message: Setup complete
506456
- type: OpenStackLightspeedMCPServerReady
507457
status: "True"
458+
message: "RHOSO MCP server is disabled (rhoso_mcps feature flag not set)"
508459
- type: OpenStackLightspeedReady
509460
status: "True"
510461
reason: Ready

0 commit comments

Comments
 (0)