Skip to content

Commit 7f84ade

Browse files
committed
updates
1 parent f0d702c commit 7f84ade

7 files changed

Lines changed: 260 additions & 50 deletions

File tree

api/assistant/v1beta1/openstackassistant_types.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,24 @@ type LightspeedStackSpec struct {
5252
CaBundleSecretName string `json:"caBundleSecretName,omitempty"`
5353
}
5454

55-
// MCPServerRef references an MCP server endpoint to configure as a Goose extension
55+
// MCPServerRef references an MCP server endpoint to configure as a Goose extension.
56+
// Either URL or OpenStackClientRef must be specified, but not both.
5657
type MCPServerRef struct {
5758
// Name is the extension name in Goose config
5859
// +kubebuilder:validation:Required
5960
Name string `json:"name"`
6061

61-
// URL is the MCP server's Streamable HTTP endpoint
62-
// (e.g. http://openstackclient-mcp.openstack.svc:8080/openstack/)
63-
// +kubebuilder:validation:Required
64-
URL string `json:"url"`
62+
// URL is the MCP server's Streamable HTTP endpoint.
63+
// Mutually exclusive with OpenStackClientRef.
64+
// +kubebuilder:validation:Optional
65+
URL string `json:"url,omitempty"`
66+
67+
// OpenStackClientRef is the name of an OpenStackClient CR in the same
68+
// namespace that has MCP enabled. The controller auto-computes the
69+
// correct service URL and TLS CA configuration.
70+
// Mutually exclusive with URL.
71+
// +kubebuilder:validation:Optional
72+
OpenStackClientRef string `json:"openstackClientRef,omitempty"`
6573
}
6674

6775
// GooseConfig defines Goose-specific provider configuration

api/bases/assistant.openstack.org_openstackassistants.yaml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,20 +192,27 @@ spec:
192192
description: MCPServers lists MCP server endpoints to configure
193193
as Goose extensions.
194194
items:
195-
description: MCPServerRef references an MCP server endpoint
196-
to configure as a Goose extension
195+
description: |-
196+
MCPServerRef references an MCP server endpoint to configure as a Goose extension.
197+
Either URL or OpenStackClientRef must be specified, but not both.
197198
properties:
198199
name:
199200
description: Name is the extension name in Goose config
200201
type: string
202+
openstackClientRef:
203+
description: |-
204+
OpenStackClientRef is the name of an OpenStackClient CR in the same
205+
namespace that has MCP enabled. The controller auto-computes the
206+
correct service URL and TLS CA configuration.
207+
Mutually exclusive with URL.
208+
type: string
201209
url:
202210
description: |-
203-
URL is the MCP server's Streamable HTTP endpoint
204-
(e.g. http://openstackclient-mcp.openstack.svc:8080/openstack/)
211+
URL is the MCP server's Streamable HTTP endpoint.
212+
Mutually exclusive with OpenStackClientRef.
205213
type: string
206214
required:
207215
- name
208-
- url
209216
type: object
210217
type: array
211218
model:

bindata/crds/crds.yaml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -191,20 +191,27 @@ spec:
191191
description: MCPServers lists MCP server endpoints to configure
192192
as Goose extensions.
193193
items:
194-
description: MCPServerRef references an MCP server endpoint
195-
to configure as a Goose extension
194+
description: |-
195+
MCPServerRef references an MCP server endpoint to configure as a Goose extension.
196+
Either URL or OpenStackClientRef must be specified, but not both.
196197
properties:
197198
name:
198199
description: Name is the extension name in Goose config
199200
type: string
201+
openstackClientRef:
202+
description: |-
203+
OpenStackClientRef is the name of an OpenStackClient CR in the same
204+
namespace that has MCP enabled. The controller auto-computes the
205+
correct service URL and TLS CA configuration.
206+
Mutually exclusive with URL.
207+
type: string
200208
url:
201209
description: |-
202-
URL is the MCP server's Streamable HTTP endpoint
203-
(e.g. http://openstackclient-mcp.openstack.svc:8080/openstack/)
210+
URL is the MCP server's Streamable HTTP endpoint.
211+
Mutually exclusive with OpenStackClientRef.
204212
type: string
205213
required:
206214
- name
207-
- url
208215
type: object
209216
type: array
210217
model:

config/crd/bases/assistant.openstack.org_openstackassistants.yaml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,20 +192,27 @@ spec:
192192
description: MCPServers lists MCP server endpoints to configure
193193
as Goose extensions.
194194
items:
195-
description: MCPServerRef references an MCP server endpoint
196-
to configure as a Goose extension
195+
description: |-
196+
MCPServerRef references an MCP server endpoint to configure as a Goose extension.
197+
Either URL or OpenStackClientRef must be specified, but not both.
197198
properties:
198199
name:
199200
description: Name is the extension name in Goose config
200201
type: string
202+
openstackClientRef:
203+
description: |-
204+
OpenStackClientRef is the name of an OpenStackClient CR in the same
205+
namespace that has MCP enabled. The controller auto-computes the
206+
correct service URL and TLS CA configuration.
207+
Mutually exclusive with URL.
208+
type: string
201209
url:
202210
description: |-
203-
URL is the MCP server's Streamable HTTP endpoint
204-
(e.g. http://openstackclient-mcp.openstack.svc:8080/openstack/)
211+
URL is the MCP server's Streamable HTTP endpoint.
212+
Mutually exclusive with OpenStackClientRef.
205213
type: string
206214
required:
207215
- name
208-
- url
209216
type: object
210217
type: array
211218
model:

internal/controller/assistant/openstackassistant_controller.go

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import (
5353
"github.com/openstack-k8s-operators/lib-common/modules/common/util"
5454

5555
assistantv1 "github.com/openstack-k8s-operators/openstack-operator/api/assistant/v1beta1"
56+
clientv1 "github.com/openstack-k8s-operators/openstack-operator/api/client/v1beta1"
5657
"github.com/openstack-k8s-operators/openstack-operator/internal/openstackassistant"
5758
)
5859

@@ -81,6 +82,7 @@ func (r *OpenStackAssistantReconciler) GetLogger(ctx context.Context) logr.Logge
8182
// +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=rolebindings,verbs=get;list;watch;create;update;patch
8283
// +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=clusterroles,verbs=get;list;watch;create;update;patch;delete
8384
// +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=clusterrolebindings,verbs=get;list;watch;create;update;patch;delete
85+
// +kubebuilder:rbac:groups=client.openstack.org,resources=openstackclients,verbs=get;list;watch
8486

8587
// Reconcile -
8688
func (r *OpenStackAssistantReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, _err error) {
@@ -223,7 +225,7 @@ func (r *OpenStackAssistantReconciler) Reconcile(ctx context.Context, req ctrl.R
223225
condition.ErrorReason,
224226
condition.SeverityWarning,
225227
assistantv1.OpenStackAssistantReadyErrorMessage,
226-
fmt.Sprintf("CA bundle secret %s not found", instance.Spec.LightspeedStack.CaBundleSecretName)))
228+
"CA bundle secret "+instance.Spec.LightspeedStack.CaBundleSecretName))
227229
return ctrl.Result{}, nil
228230
}
229231
return ctrl.Result{}, err
@@ -288,8 +290,74 @@ func (r *OpenStackAssistantReconciler) Reconcile(ctx context.Context, req ctrl.R
288290
return ctrl.Result{}, err
289291
}
290292

293+
// Resolve MCP servers (auto-discover from OpenStackClientRef or use manual URL)
294+
resolvedMCPServers := make(map[string]string)
295+
mcpCaBundleSecretName := ""
296+
if instance.Spec.Goose != nil {
297+
for _, mcp := range instance.Spec.Goose.MCPServers {
298+
if mcp.OpenStackClientRef != "" {
299+
osclient := &clientv1.OpenStackClient{}
300+
err := r.Get(ctx, types.NamespacedName{
301+
Name: mcp.OpenStackClientRef,
302+
Namespace: instance.Namespace,
303+
}, osclient)
304+
if err != nil {
305+
if k8s_errors.IsNotFound(err) {
306+
instance.Status.Conditions.Set(condition.FalseCondition(
307+
assistantv1.OpenStackAssistantReadyCondition,
308+
condition.RequestedReason,
309+
condition.SeverityInfo,
310+
"Waiting for OpenStackClient %s", mcp.OpenStackClientRef))
311+
return ctrl.Result{RequeueAfter: time.Duration(10) * time.Second}, nil
312+
}
313+
return ctrl.Result{}, fmt.Errorf("error looking up OpenStackClient %s: %w", mcp.OpenStackClientRef, err)
314+
}
315+
316+
if osclient.Spec.MCP == nil || !osclient.Spec.MCP.Enabled {
317+
instance.Status.Conditions.Set(condition.FalseCondition(
318+
assistantv1.OpenStackAssistantReadyCondition,
319+
condition.ErrorReason,
320+
condition.SeverityWarning,
321+
assistantv1.OpenStackAssistantReadyErrorMessage,
322+
"OpenStackClient "+mcp.OpenStackClientRef+" does not have MCP enabled"))
323+
return ctrl.Result{}, nil
324+
}
325+
326+
mcpSvcName := mcp.OpenStackClientRef + "-mcp"
327+
scheme := "http"
328+
if osclient.Spec.CaBundleSecretName != "" {
329+
scheme = "https"
330+
mcpCaBundleSecretName = osclient.Spec.CaBundleSecretName
331+
}
332+
mcpURL := fmt.Sprintf("%s://%s.%s.svc:8080/openstack/", scheme, mcpSvcName, instance.Namespace)
333+
resolvedMCPServers[mcp.Name] = mcpURL
334+
Log.Info("Auto-resolved MCP server", "name", mcp.Name, "url", mcpURL, "openstackClientRef", mcp.OpenStackClientRef)
335+
} else if mcp.URL != "" {
336+
resolvedMCPServers[mcp.Name] = mcp.URL
337+
}
338+
}
339+
}
340+
341+
// Validate MCP CA bundle secret if auto-discovered
342+
if mcpCaBundleSecretName != "" && mcpCaBundleSecretName != instance.Spec.LightspeedStack.CaBundleSecretName {
343+
_, mcpCaHash, err := secret.GetSecret(ctx, helper, mcpCaBundleSecretName, instance.Namespace)
344+
if err != nil {
345+
if k8s_errors.IsNotFound(err) {
346+
instance.Status.Conditions.Set(condition.FalseCondition(
347+
assistantv1.OpenStackAssistantReadyCondition,
348+
condition.ErrorReason,
349+
condition.SeverityWarning,
350+
assistantv1.OpenStackAssistantReadyErrorMessage,
351+
"MCP CA bundle secret "+mcpCaBundleSecretName+" not found"))
352+
return ctrl.Result{}, nil
353+
}
354+
return ctrl.Result{}, err
355+
}
356+
configVars["mcp-ca-bundle"] = env.SetValue(mcpCaHash)
357+
}
358+
291359
// Build PodSpec
292-
spec := openstackassistant.AssistantPodSpec(instance, configVarsHash)
360+
spec := openstackassistant.AssistantPodSpec(instance, configVarsHash, resolvedMCPServers, mcpCaBundleSecretName)
293361

294362
podSpecHash, err := util.ObjectHash(spec)
295363
if err != nil {
@@ -623,9 +691,47 @@ func (r *OpenStackAssistantReconciler) SetupWithManager(
623691
handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc),
624692
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
625693
).
694+
Watches(
695+
&clientv1.OpenStackClient{},
696+
handler.EnqueueRequestsFromMapFunc(r.findAssistantsForOpenStackClient),
697+
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
698+
).
626699
Complete(r)
627700
}
628701

702+
func (r *OpenStackAssistantReconciler) findAssistantsForOpenStackClient(ctx context.Context, src client.Object) []reconcile.Request {
703+
Log := r.GetLogger(ctx)
704+
requests := []reconcile.Request{}
705+
706+
crList := &assistantv1.OpenStackAssistantList{}
707+
if err := r.List(ctx, crList, client.InNamespace(src.GetNamespace())); err != nil {
708+
Log.Error(err, "listing OpenStackAssistants for OpenStackClient change")
709+
return requests
710+
}
711+
712+
for _, item := range crList.Items {
713+
if item.Spec.Goose == nil {
714+
continue
715+
}
716+
for _, mcp := range item.Spec.Goose.MCPServers {
717+
if mcp.OpenStackClientRef == src.GetName() {
718+
Log.Info("OpenStackClient changed, reconciling assistant",
719+
"openstackClient", src.GetName(),
720+
"assistant", item.GetName())
721+
requests = append(requests, reconcile.Request{
722+
NamespacedName: types.NamespacedName{
723+
Name: item.GetName(),
724+
Namespace: item.GetNamespace(),
725+
},
726+
})
727+
break
728+
}
729+
}
730+
}
731+
732+
return requests
733+
}
734+
629735
func (r *OpenStackAssistantReconciler) findObjectsForSrc(ctx context.Context, src client.Object) []reconcile.Request {
630736
requests := []reconcile.Request{}
631737

internal/openstackassistant/funcs.go

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,31 @@ if [ -f /tmp/lightspeed-provider/lightspeed.json ]; then
102102
cp /tmp/lightspeed-provider/lightspeed.json $HOME/.config/goose/custom_providers/lightspeed.json
103103
fi
104104
105+
# Combine CA bundles if MCP CA is present alongside Lightspeed CA
106+
if [ -f /etc/ssl/certs/mcp-ca-bundle.crt ]; then
107+
if [ -f /etc/ssl/certs/ca-certificates.crt ]; then
108+
cp /etc/ssl/certs/ca-certificates.crt /tmp/combined-ca.crt
109+
cat /etc/ssl/certs/mcp-ca-bundle.crt >> /tmp/combined-ca.crt
110+
export SSL_CERT_FILE=/tmp/combined-ca.crt
111+
else
112+
export SSL_CERT_FILE=/etc/ssl/certs/mcp-ca-bundle.crt
113+
fi
114+
fi
115+
105116
exec sleep infinity
106117
`
107118
}
108119

109-
// AssistantPodSpec returns the PodSpec for the assistant pod
120+
// AssistantPodSpec returns the PodSpec for the assistant pod.
121+
// resolvedMCPServers maps extension name to URL for all MCP servers
122+
// (both manually specified and auto-resolved from OpenStackClientRef).
123+
// mcpCaBundleSecretName is the CA bundle secret for MCP TLS connections,
124+
// auto-discovered from the referenced OpenStackClient.
110125
func AssistantPodSpec(
111126
instance *assistantv1.OpenStackAssistant,
112127
configHash string,
128+
resolvedMCPServers map[string]string,
129+
mcpCaBundleSecretName string,
113130
) corev1.PodSpec {
114131
envVars := map[string]env.Setter{}
115132
envVars["CONFIG_HASH"] = env.SetValue(configHash)
@@ -125,10 +142,8 @@ func AssistantPodSpec(
125142
envVars["GOOSE_MODEL"] = env.SetValue(instance.Spec.Goose.Model)
126143
}
127144

128-
if instance.Spec.Goose != nil {
129-
for _, mcp := range instance.Spec.Goose.MCPServers {
130-
envVars["MCP_SERVER_"+mcp.Name] = env.SetValue(mcp.URL)
131-
}
145+
for name, url := range resolvedMCPServers {
146+
envVars["MCP_SERVER_"+name] = env.SetValue(url)
132147
}
133148

134149
if instance.Spec.Env != nil {
@@ -141,8 +156,8 @@ func AssistantPodSpec(
141156
}
142157
}
143158

144-
volumes := assistantPodVolumes(instance)
145-
volumeMounts := assistantPodVolumeMounts(instance)
159+
volumes := assistantPodVolumes(instance, mcpCaBundleSecretName)
160+
volumeMounts := assistantPodVolumeMounts(instance, mcpCaBundleSecretName)
146161

147162
containerName := "goose"
148163
if instance.Spec.Provider != "" {
@@ -181,7 +196,7 @@ func AssistantPodSpec(
181196
return podSpec
182197
}
183198

184-
func assistantPodVolumeMounts(instance *assistantv1.OpenStackAssistant) []corev1.VolumeMount {
199+
func assistantPodVolumeMounts(instance *assistantv1.OpenStackAssistant, mcpCaBundleSecretName string) []corev1.VolumeMount {
185200
mounts := []corev1.VolumeMount{
186201
{
187202
Name: "entrypoint",
@@ -221,10 +236,19 @@ func assistantPodVolumeMounts(instance *assistantv1.OpenStackAssistant) []corev1
221236
})
222237
}
223238

239+
if mcpCaBundleSecretName != "" && mcpCaBundleSecretName != instance.Spec.LightspeedStack.CaBundleSecretName {
240+
mounts = append(mounts, corev1.VolumeMount{
241+
Name: "mcp-ca-bundle",
242+
MountPath: "/etc/ssl/certs/mcp-ca-bundle.crt",
243+
SubPath: "ca-bundle.crt",
244+
ReadOnly: true,
245+
})
246+
}
247+
224248
return mounts
225249
}
226250

227-
func assistantPodVolumes(instance *assistantv1.OpenStackAssistant) []corev1.Volume {
251+
func assistantPodVolumes(instance *assistantv1.OpenStackAssistant, mcpCaBundleSecretName string) []corev1.Volume {
228252
volumes := []corev1.Volume{
229253
{
230254
Name: "entrypoint",
@@ -285,5 +309,16 @@ func assistantPodVolumes(instance *assistantv1.OpenStackAssistant) []corev1.Volu
285309
})
286310
}
287311

312+
if mcpCaBundleSecretName != "" && mcpCaBundleSecretName != instance.Spec.LightspeedStack.CaBundleSecretName {
313+
volumes = append(volumes, corev1.Volume{
314+
Name: "mcp-ca-bundle",
315+
VolumeSource: corev1.VolumeSource{
316+
Secret: &corev1.SecretVolumeSource{
317+
SecretName: mcpCaBundleSecretName,
318+
},
319+
},
320+
})
321+
}
322+
288323
return volumes
289324
}

0 commit comments

Comments
 (0)