Skip to content

Commit fcc98d8

Browse files
committed
Add OpenStackAssistant CRD with MCP server support
Introduces a new OpenStackAssistant custom resource (assistant.openstack.org/v1beta1) that deploys a managed Goose AI agent pod for cluster diagnostics via Lightspeed Stack. OpenStackAssistant CRD and controller: - New CRD with spec fields for provider type, container image, Lightspeed Stack backend configuration, node selectors, and additional environment variables - GooseConfig supports model selection, recipe ConfigMaps (registered as Goose slash commands), hints ConfigMaps (written to .goosehints), and MCP server references - Controller creates a ServiceAccount, ClusterRole with read-only RBAC for cluster diagnostics, ClusterRoleBinding, ConfigMap with Goose configuration and entrypoint script, and the assistant Pod - Watches referenced Secrets and ConfigMaps; reconciles on changes and tracks input hashes to detect drift - Defaulting webhook sets the container image from an environment variable fallback - Condition-based status reporting (ServiceAccount, RBAC, ConfigMap, Pod readiness) MCP server sidecar support for OpenStackClient: - New MCPConfig struct (enabled flag, containerImage) on the OpenStackClient CR spec - When enabled, the OpenStackClient controller adds a rhos-mcps MCP server sidecar container sharing the same clouds.yaml/secure.yaml credential mounts - Controller creates a ConfigMap with rhos-mcps config (openstack enabled, openshift disabled, allow_write: false) and a Service on port 8080 for the MCP endpoint - OpenStackAssistant can reference an OpenStackClient CR by name via the openstackClientRef field; the controller auto-computes the service URL and TLS CA configuration Tests: - Unit tests for the OpenStackAssistant controller covering reconciliation, pod creation, config generation, and status conditions - Unit tests for helper functions (entrypoint script generation, config building, hash computation) 44 files changed, ~4,300 lines added.
1 parent 918749d commit fcc98d8

37 files changed

Lines changed: 3675 additions & 150 deletions
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
Licensed under the Apache License, Version 2.0 (the "License");
3+
you may not use this file except in compliance with the License.
4+
You may obtain a copy of the License at
5+
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
*/
14+
15+
package v1beta1
16+
17+
import (
18+
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
19+
)
20+
21+
// OpenStackAssistant Condition Types used by API objects.
22+
const (
23+
// OpenStackAssistantReadyCondition Status=True condition which indicates if OpenStackAssistant is configured and operational
24+
OpenStackAssistantReadyCondition condition.Type = "OpenStackAssistantReady"
25+
)
26+
27+
// Common Messages used by API objects.
28+
const (
29+
// OpenStackAssistantReadyInitMessage
30+
OpenStackAssistantReadyInitMessage = "OpenStack Assistant not started"
31+
32+
// OpenStackAssistantReadyRunningMessage
33+
OpenStackAssistantReadyRunningMessage = "OpenStack Assistant in progress"
34+
35+
// OpenStackAssistantReadyMessage
36+
OpenStackAssistantReadyMessage = "OpenStack Assistant created"
37+
38+
// OpenStackAssistantReadyErrorMessage
39+
OpenStackAssistantReadyErrorMessage = "OpenStack Assistant error occured %s"
40+
41+
// OpenStackAssistantProviderSecretWaitingMessage
42+
OpenStackAssistantProviderSecretWaitingMessage = "Waiting for lightspeed provider secret"
43+
44+
// OpenStackAssistantRecipesWaitingMessage
45+
OpenStackAssistantRecipesWaitingMessage = "Waiting for Goose recipes ConfigMap"
46+
47+
// OpenStackAssistantHintsWaitingMessage
48+
OpenStackAssistantHintsWaitingMessage = "Waiting for Goose hints ConfigMap"
49+
)

api/assistant/v1beta1/groupversion_info.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2022.
2+
Copyright 2026.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.

api/assistant/v1beta1/openstackassistant_types.go

Lines changed: 169 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2022.
2+
Copyright 2026.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -17,31 +17,139 @@ limitations under the License.
1717
package v1beta1
1818

1919
import (
20+
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
21+
"github.com/openstack-k8s-operators/lib-common/modules/common/util"
22+
corev1 "k8s.io/api/core/v1"
2023
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2124
)
2225

23-
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
24-
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
26+
const (
27+
// OpenStackAssistantContainerImage is the fall-back container image for OpenStackAssistant
28+
OpenStackAssistantContainerImage = "quay.io/dprince/goose:oc-fedora"
29+
)
30+
31+
// ProviderType defines the AI agent provider
32+
// +kubebuilder:validation:Enum=goose
33+
type ProviderType string
34+
35+
const (
36+
// ProviderGoose is the Goose AI agent provider
37+
ProviderGoose ProviderType = "goose"
38+
)
39+
40+
// LightspeedStackSpec defines connectivity to the Lightspeed Stack AI backend
41+
type LightspeedStackSpec struct {
42+
// ProviderSecret is the name of a Secret containing the lightspeed
43+
// provider config JSON (custom_providers/lightspeed.json content).
44+
// Must contain key "lightspeed.json".
45+
// +kubebuilder:validation:Required
46+
ProviderSecret string `json:"providerSecret"`
47+
48+
// CaBundleSecretName is the name of a Secret containing CA certs
49+
// to trust for TLS connections to the lightspeed-stack endpoint.
50+
// The Secret must contain a key "ca-bundle.crt" with PEM-encoded certs.
51+
// +kubebuilder:validation:Optional
52+
CaBundleSecretName string `json:"caBundleSecretName,omitempty"`
53+
}
54+
55+
// MCPServerRef references an MCP server endpoint to configure as a Goose extension.
56+
// Either URL or OpenStackClientRef must be specified, but not both.
57+
type MCPServerRef struct {
58+
// Name is the extension name in Goose config
59+
// +kubebuilder:validation:Required
60+
Name string `json:"name"`
61+
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"`
73+
}
2574

26-
// OpenStackAssistantSpec defines the desired state of OpenStackAssistant.
75+
// GooseConfig defines Goose-specific provider configuration
76+
type GooseConfig struct {
77+
// Model is the model identifier for the Goose AI agent
78+
// (e.g., "gemini/models/gemini-2.5-flash"). Sets the GOOSE_MODEL env var.
79+
// +kubebuilder:validation:Optional
80+
Model string `json:"model,omitempty"`
81+
82+
// Recipes is a ConfigMap name containing Goose recipe YAML files.
83+
// Each key in the ConfigMap becomes a recipe file registered as a
84+
// Goose slash command (e.g., /cluster-health).
85+
// +kubebuilder:validation:Optional
86+
Recipes *string `json:"recipes,omitempty"`
87+
88+
// Hints is a ConfigMap name containing Goose hints/context.
89+
// The ConfigMap must have a key "hints" with the content that
90+
// will be written to ~/.goosehints in the pod.
91+
// +kubebuilder:validation:Optional
92+
Hints *string `json:"hints,omitempty"`
93+
94+
// MCPServers lists MCP server endpoints to configure as Goose extensions.
95+
// +kubebuilder:validation:Optional
96+
MCPServers []MCPServerRef `json:"mcpServers,omitempty"`
97+
}
98+
99+
// OpenStackAssistantSpec defines the desired state of OpenStackAssistant
27100
type OpenStackAssistantSpec struct {
28-
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
29-
// Important: Run "make" to regenerate code after modifying this file
101+
// ContainerImage for the assistant container (will be set to environmental default if empty).
102+
// +kubebuilder:validation:Optional
103+
ContainerImage string `json:"containerImage,omitempty"`
30104

31-
// Foo is an example field of OpenStackAssistant. Edit openstackassistant_types.go to remove/update
32-
Foo string `json:"foo,omitempty"`
105+
// Provider is the AI agent provider type. Currently only "goose" is supported.
106+
// +kubebuilder:validation:Optional
107+
// +kubebuilder:default=goose
108+
Provider ProviderType `json:"provider"`
109+
110+
// LightspeedStack configuration for the AI backend.
111+
// +kubebuilder:validation:Required
112+
LightspeedStack LightspeedStackSpec `json:"lightspeedStack"`
113+
114+
// Goose contains Goose-specific provider configuration.
115+
// Only applicable when provider is "goose".
116+
// +kubebuilder:validation:Optional
117+
Goose *GooseConfig `json:"goose,omitempty"`
118+
119+
// NodeSelector to target subset of worker nodes for pod scheduling.
120+
// +kubebuilder:validation:Optional
121+
NodeSelector *map[string]string `json:"nodeSelector,omitempty"`
122+
123+
// Env is a list of additional environment variables for the container.
124+
// +kubebuilder:validation:Optional
125+
// +listType=map
126+
// +listMapKey=name
127+
Env []corev1.EnvVar `json:"env,omitempty"`
33128
}
34129

35-
// OpenStackAssistantStatus defines the observed state of OpenStackAssistant.
130+
// OpenStackAssistantStatus defines the observed state of OpenStackAssistant
36131
type OpenStackAssistantStatus struct {
37-
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
38-
// Important: Run "make" to regenerate code after modifying this file
132+
// PodName is the name of the running assistant pod
133+
PodName string `json:"podName,omitempty"`
134+
135+
// Conditions tracks the state of each sub-resource
136+
Conditions condition.Conditions `json:"conditions,omitempty" optional:"true"`
137+
138+
// ObservedGeneration - the most recent generation observed
139+
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
140+
141+
// Hash tracks input hashes to detect changes
142+
Hash map[string]string `json:"hash,omitempty"`
39143
}
40144

41145
// +kubebuilder:object:root=true
42146
// +kubebuilder:subresource:status
147+
// +operator-sdk:csv:customresourcedefinitions:displayName="OpenStack Assistant"
148+
// +kubebuilder:resource:shortName=osassistant;osassistants
149+
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[0].status",description="Status"
150+
// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[0].message",description="Message"
43151

44-
// OpenStackAssistant is the Schema for the openstackassistants API.
152+
// OpenStackAssistant is the Schema for the openstackassistants API
45153
type OpenStackAssistant struct {
46154
metav1.TypeMeta `json:",inline"`
47155
metav1.ObjectMeta `json:"metadata,omitempty"`
@@ -52,7 +160,7 @@ type OpenStackAssistant struct {
52160

53161
// +kubebuilder:object:root=true
54162

55-
// OpenStackAssistantList contains a list of OpenStackAssistant.
163+
// OpenStackAssistantList contains a list of OpenStackAssistant
56164
type OpenStackAssistantList struct {
57165
metav1.TypeMeta `json:",inline"`
58166
metav1.ListMeta `json:"metadata,omitempty"`
@@ -62,3 +170,51 @@ type OpenStackAssistantList struct {
62170
func init() {
63171
SchemeBuilder.Register(&OpenStackAssistant{}, &OpenStackAssistantList{})
64172
}
173+
174+
// IsReady - returns true if OpenStackAssistant is reconciled successfully
175+
func (instance OpenStackAssistant) IsReady() bool {
176+
return instance.Status.Conditions.IsTrue(OpenStackAssistantReadyCondition)
177+
}
178+
179+
// RbacConditionsSet - set the conditions for the rbac object
180+
func (instance OpenStackAssistant) RbacConditionsSet(c *condition.Condition) {
181+
instance.Status.Conditions.Set(c)
182+
}
183+
184+
// RbacNamespace - return the namespace
185+
func (instance OpenStackAssistant) RbacNamespace() string {
186+
return instance.Namespace
187+
}
188+
189+
// RbacResourceName - return the name to be used for rbac objects (serviceaccount, role, rolebinding)
190+
func (instance OpenStackAssistant) RbacResourceName() string {
191+
return "openstackassistant-" + instance.Name
192+
}
193+
194+
// OpenStackAssistantDefaults holds defaults for the assistant
195+
type OpenStackAssistantDefaults struct {
196+
ContainerImageURL string
197+
}
198+
199+
var openStackAssistantDefaults OpenStackAssistantDefaults
200+
201+
// SetupOpenStackAssistantDefaults - initialize OpenStackAssistant spec defaults
202+
func SetupOpenStackAssistantDefaults(defaults OpenStackAssistantDefaults) {
203+
openStackAssistantDefaults = defaults
204+
}
205+
206+
// SetupDefaults - initializes any CRD field defaults based on environment variables
207+
func SetupDefaults() {
208+
openStackAssistantDefaults := OpenStackAssistantDefaults{
209+
ContainerImageURL: util.GetEnvVar("RELATED_IMAGE_OPENSTACK_ASSISTANT_IMAGE_URL_DEFAULT", OpenStackAssistantContainerImage),
210+
}
211+
212+
SetupOpenStackAssistantDefaults(openStackAssistantDefaults)
213+
}
214+
215+
// Default implements webhook.Defaulter
216+
func (r *OpenStackAssistant) Default() {
217+
if r.Spec.ContainerImage == "" {
218+
r.Spec.ContainerImage = openStackAssistantDefaults.ContainerImageURL
219+
}
220+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1beta1
18+
19+
import (
20+
"k8s.io/apimachinery/pkg/runtime"
21+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
22+
)
23+
24+
// ValidateCreate implements webhook.Validator
25+
func (r *OpenStackAssistant) ValidateCreate() (admission.Warnings, error) {
26+
return nil, nil
27+
}
28+
29+
// ValidateUpdate implements webhook.Validator
30+
func (r *OpenStackAssistant) ValidateUpdate(_ runtime.Object) (admission.Warnings, error) {
31+
return nil, nil
32+
}
33+
34+
// ValidateDelete implements webhook.Validator
35+
func (r *OpenStackAssistant) ValidateDelete() (admission.Warnings, error) {
36+
return nil, nil
37+
}

0 commit comments

Comments
 (0)