Skip to content

Lcore mcps#19

Open
Akrog wants to merge 4 commits into
openstack-k8s-operators:mainfrom
Akrog:lcore-mcps
Open

Lcore mcps#19
Akrog wants to merge 4 commits into
openstack-k8s-operators:mainfrom
Akrog:lcore-mcps

Conversation

@Akrog

@Akrog Akrog commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

This PR adds support for the rhos-mcps image that provides openstack-cli and openshift-cli tools.

The MCP server is deployed as a sidecar container with the openshift-cli tool always enabled and the openstack-cli tool only enabled once the OpenStackControlPlane is ready.

By default the rhos-mcps is not deployed and requires the rhos_mcps feature flag to be enabled.

@openshift-ci
openshift-ci Bot requested review from lpiwowar and umago July 14, 2026 16:30
@openshift-ci

openshift-ci Bot commented Jul 14, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: Akrog

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci

openshift-ci Bot commented Jul 21, 2026

Copy link
Copy Markdown

@Akrog: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/openstack-lightspeed-kuttl-4-18 bd2487c link true /test openstack-lightspeed-kuttl-4-18

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@Akrog

Akrog commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

/retest

With this commit we start deploying the MCP server as a sidecar
container of the lightspeed-service container.

The MCP server deployed is the one from our own rhos-mcps repository
[1].

On installation the `openshift-cli` tool is enabled when OpenStack
Lightspeed is configured, and the `openstack-cli` tool is enabled when
the `OpenStackControlPlane` is ready.

From a security perspective since we are deploying the MCP tools in the
pod's network namespace not using TLS is not a real security risk.

[1]: https://github.com/openstack-lightspeed/rhos-mcps

Jira: OSPRH-27075
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Akrog and others added 3 commits July 22, 2026 10:59
Job govulncheck is failing with the following error.

```
Vulnerability openstack-k8s-operators#1: GO-2026-5970
    Infinite loop on invalid input in golang.org/x/text
  More info: https://pkg.go.dev/vuln/GO-2026-5970
  Module: golang.org/x/text
    Found in: golang.org/x/text@v0.37.0
    Fixed in: golang.org/x/text@v0.39.0
```

This commit updates the text dependency and transitive version bumps of
other dependencies.
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>
Initial implementation of the MCP deployment uses the credentials from
the `openstackclient` pod, which means that we are not the owners of
that secret, just the copy we make in the `openstack-lightspeed`
namespace, so those credentials could be removed/deleted and that would
break our `openstack-cli` tool.

In this patch we change the credentials and we leverage the
`KeystoneApplicationCredential` CR to get our own credentials.

Credential Rotation is handled by the code as well.

Jira: OSPRH-27075
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Akrog

Akrog commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

I have added another commit to fix the vulnerability reported by the CI job

@lpiwowar lpiwowar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM!:) Thank you!

I wrote a comment for anything that crossed my mind not everything is blocking.

I personally would like to resolve these two things somehow (either by an agreement or by change in the code) before we merge:

  • MCP image handling in downstream and pullspecs_repleceatron.sh (setting to empty string)
  • The usage of RawClient() and question whether we should not add openstack into WATCH_NAMESPACES

Comment thread api/v1beta1/conditions.go
OpenStackLightspeedMCPServerWaitingAC = "Waiting for application credential secret"

// OpenStackLightspeedMCPServerDisabledMessage
OpenStackLightspeedMCPServerDisabledMessage = "RHOS MCP server is disabled (rhos_mcps feature flag not set)"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (non-blocking): Shouldn't we use "RHOSO MCP" server or "OpenStack Lightspeed MCP server"? I feel like the "RHOS" naming is something we do not use much.

OKPContainerImage = "registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest"

// MCPServerContainerImage is the fall-back container image for the MCP server
MCPServerContainerImage = "quay.io/openstack-lightspeed/rhos-mcps:latest"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (blocking): Do we know how we are going to deal with the image downstream? Related to the conversation we already had. Suggested solution was to use empty string downstream for the RELATED_IMAGE_MCP_* environment variable. The issue is that the the pullspecs_repleceatron.sh ends with an error when any RELATED_IMAGE_* env var is an empty string. In my opinion before we merge we should:

  • Update pullspecs_repleceatron.sh to make it work with empty string for RELATED_IMAGE_* env vars.
  • Check with somebody an empty string for RELATED_IMAGE_* will be accepted well downstream.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we can discuss this with Miguel ? He seems pretty open to suggestions

Comment thread api/v1beta1/openstacklightspeed_types.go
Comment thread go.mod
// must be consistent within modules and service operators
replace github.com/openshift/api => github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e

require k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (non-blocking): Why this can not be in the require block below together with all the other // indirect dependencies?


// getSecretResourceVersion retrieves the resource version of a Secret.
func getSecretResourceVersion(ctx context.Context, h *common_helper.Helper, name string, namespace string) (string, error) {
rawClient, err := getRawClient(h)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): I think we should really consider adding openstack namespace into namespaces that are watched by default. That way it is obvious what namespaces the operator watches and we do need to bypass the WATCH_NAMESPACES with raw client. I think it would be a step in the right direction. If for whatever reason the OpenStack controlplane lives in a different namespace then the customer can control that with OperatorGroup.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this suggestion as I am a bit worry about the whole cross-namespacing aspects of things. I think that we could have it namespaced for now even if it's a tech debt for multi-namespaces (or non default namespaces) of the OpenStack depoyment until we have something else figure out. More like, "security first" approach for this first implementation.

Comment on lines +10 to +17
set -euo pipefail
READY=$(oc get openstacklightspeed openstack-lightspeed \
-n openstack-lightspeed \
-o jsonpath='{.status.openStackReady}' 2>/dev/null || true)
if [ "$READY" = "true" ]; then
echo "ERROR: openStackReady is still true"
exit 1
fi

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): This can probably be:

  apiVersion: lightspeed.openstack.org/v1beta1
  kind: OpenStackLightspeed
  metadata:
    name: openstack-lightspeed
    namespace: openstack-lightspeed
  status:
    conditions:
      - type: Ready
        status: "True"

Comment on lines +19 to +26
MCP_DATA=$(oc get configmap mcp-config -n openstack-lightspeed \
-o jsonpath='{.data.config\.yaml}')
ENABLED=$(echo "$MCP_DATA" | grep -A1 "openstack:" | grep "enabled:" | tr -d ' ')
if [ "$ENABLED" != "enabled:false" ]; then
echo "ERROR: MCP config does not have openstack disabled"
echo "$MCP_DATA"
exit 1
fi

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): If we really need to use the script (which seems to be the valid case here) I think we should move the repeating snippets into ../common/shared_funcs.go where they can be easily reused by the individual test steps:

source ../common/shared_funcs.go
ssert_mcp_openstack_enabled true / false

I think we can find some other good candidates for this.

case 0:
return nil, errors.New("no cloud entry found in clouds.yaml")
case 1:
for name = range clouds.Clouds {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (blocking): This does nothing.

}

// Delete AC CR
rawClient, err := getRawClient(helper)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): Already mentioned. I think we should ensure that our operator properly watches the openstack namespace and be transparent about it.


// cleanupMCPResources removes MCP server resources when the rhos_mcps feature
// flag is disabled.
func (r *OpenStackLightspeedReconciler) cleanupMCPResources(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): This can be simplified e.g. like this:

 func (r *OpenStackLightspeedReconciler) cleanupMCPResources(  
        ctx context.Context,  
        helper *common_helper.Helper,  
        instance *apiv1beta1.OpenStackLightspeed,  
  ) error {  
        ns := instance.Namespace  

        resources := []client.Object{  
                &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: MCPConfigYAMLConfigMapName, Namespace: ns}},  
                &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: CloudsYAMLConfigMapName, Namespace: ns}},  
                &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: SecureYAMLSecretName, Namespace: ns}},  
                &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: CombinedCABundleSecretName, Namespace: ns}},  
        }  

        for _, obj := range resources {  
                if err := helper.GetClient().Delete(ctx, obj); err != nil && !k8s_errors.IsNotFound(err) {  
                        return fmt.Errorf("failed to delete %s %s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err)  
                }  
        }  

        if err := r.reconcileDeleteOpenStackResources(ctx, helper, instance); err != nil {  
                return fmt.Errorf("failed to clean up OpenStack resources during RHOS MCP disable: %w", err)  
        }  

        helper.GetLogger().Info("RHOS MCP resources cleaned up")  
        return nil  
  }  

@umago umago left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Gorka! Overall I think it looks solid, I'm a bit worried about the wide scope of some aspects of this change (commented inline). I wonder if we could go a bit more "namespaced-scoped" from the start rather than having to tight up things later.

OKPContainerImage = "registry.redhat.io/offline-knowledge-portal/rhokp-rhel9:latest"

// MCPServerContainerImage is the fall-back container image for the MCP server
MCPServerContainerImage = "quay.io/openstack-lightspeed/rhos-mcps:latest"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we can discuss this with Miguel ? He seems pretty open to suggestions

return err
}

userID, err := osClient.CreateUser(log, openstack_lib.User{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is an issue or not, but we are calling CreateUser on every reconciliation. Does keystone handles it properly ?

Comment thread config/rbac/role.yaml
verbs:
- create
- get
- update

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comes from the old coderabbit review. I don't know if there's a way around it, maybe we can assume openstack is always depoyed in the "openstack" namespace and scope it only to that namespace as a compromise until we have a better way of doing it ?

I worry that this rule allows the operator to read and write on all Secrets cluster wide without any namespace restriction. it does sounds a bit too broad. I wonder if something like this can/will be flagged in the security assessments we've been doing recently.

oldSecret, err := kclient.CoreV1().Secrets(oscpNamespace).Get(ctx, prevSecret, metav1.GetOptions{})
if err == nil && controllerutil.RemoveFinalizer(oldSecret, LightspeedACFinalizerName) {
if _, err := kclient.CoreV1().Secrets(oscpNamespace).Update(ctx, oldSecret, metav1.UpdateOptions{}); err != nil {
helper.GetLogger().Info("Failed to remove finalizer from old AC secret", "secret", prevSecret, "error", err)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should this be an error log instead ? (as L540 above)

ctx, instance.Status.ApplicationCredentialSecret, metav1.GetOptions{})
if err == nil && controllerutil.RemoveFinalizer(acSecret, LightspeedACFinalizerName) {
if _, err := helper.GetKClient().CoreV1().Secrets(oscpNS).Update(ctx, acSecret, metav1.UpdateOptions{}); err != nil {
log.Info("Failed to remove finalizer from AC secret", "error", err)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this should be n error instad of info

apiv1beta1.OpenStackLightspeedMCPServerDeployed,
)
} else {
instance.Status.Conditions.MarkTrue(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like this should be False instead as it is still waiting for OpenStack to be ready (OpenStackLightspeedMCPServerWaitingOpenStack L337 below)

},
ImagePullPolicy: corev1.PullIfNotPresent,
}
containers = append(containers, mcpContainer)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to add: Some liveness/readiness check for the MCP container ?


// getSecretResourceVersion retrieves the resource version of a Secret.
func getSecretResourceVersion(ctx context.Context, h *common_helper.Helper, name string, namespace string) (string, error) {
rawClient, err := getRawClient(h)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this suggestion as I am a bit worry about the whole cross-namespacing aspects of things. I think that we could have it namespaced for now even if it's a tech debt for multi-namespaces (or non default namespaces) of the OpenStack depoyment until we have something else figure out. More like, "security first" approach for this first implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants