Skip to content

Commit 89f717d

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 4472913 commit 89f717d

26 files changed

Lines changed: 314 additions & 103 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: 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 RHOS MCPs (MCP server sidecar with OpenStack/OpenShift tools)
2624
# dev:
2725
# featureFlags:
28-
# - okp
26+
# - rhos_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+
// 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
@@ -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, 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"),
213+
// MCP sidecar (only when rhos_mcps feature flag is enabled)
214+
rhosMCPEnabled, err := isRHOSMCPEnabled(instance)
215+
if err != nil {
216+
return corev1.PodTemplateSpec{}, fmt.Errorf("failed to parse dev config: %w", err)
217+
}
218+
if rhosMCPEnabled {
219+
mcpMounts := []corev1.VolumeMount{}
220+
addMCPVolumesAndMounts(&volumes, &mcpMounts, instance.Status.OpenStackReady)
221+
222+
mcpContainer := corev1.Container{
223+
Name: "rhos-mcp",
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: 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
@@ -314,7 +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: rhos-mcp
318317
volumes:
319318
- name: ogx-config
320319
configMap:
@@ -340,12 +339,6 @@ spec:
340339
- name: tls-certs
341340
secret:
342341
secretName: lightspeed-tls
343-
- name: mcp-config
344-
configMap:
345-
name: mcp-config
346-
items:
347-
- key: config.yaml
348-
path: config.yaml
349342
status:
350343
replicas: 1
351344
readyReplicas: 1
@@ -365,14 +358,6 @@ metadata:
365358
name: vector-db-scripts
366359
namespace: openstack-lightspeed
367360

368-
# MCP server resources
369-
---
370-
apiVersion: v1
371-
kind: ConfigMap
372-
metadata:
373-
name: mcp-config
374-
namespace: openstack-lightspeed
375-
376361
# Console Plugin resources
377362
---
378363
apiVersion: v1
@@ -470,6 +455,7 @@ status:
470455
message: Setup complete
471456
- type: OpenStackLightspeedMCPServerReady
472457
status: "True"
458+
message: "RHOS MCP server is disabled (rhos_mcps feature flag not set)"
473459
- type: OpenStackLightspeedReady
474460
status: "True"
475461
reason: Ready

0 commit comments

Comments
 (0)