Skip to content

Commit c837605

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)
1 parent b634a1a commit c837605

39 files changed

Lines changed: 3973 additions & 152 deletions
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
50+
// OpenStackAssistantSkillsWaitingMessage
51+
OpenStackAssistantSkillsWaitingMessage = "Waiting for Goose skills ConfigMap"
52+
)

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: 186 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,156 @@ 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-centos"
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+
// Exactly one of URL or OpenStackClientRef must be specified.
57+
// +kubebuilder:validation:XValidation:rule="has(self.url) != has(self.openstackClientRef)",message="exactly one of url or openstackClientRef must be set"
58+
type MCPServerRef struct {
59+
// Name is the extension name in Goose config. It is used to derive the
60+
// MCP_SERVER_<name> environment variable, so it must be a valid
61+
// environment-variable name: start with a letter or underscore and
62+
// contain only letters, digits, and underscores (no dashes).
63+
// +kubebuilder:validation:Required
64+
// +kubebuilder:validation:MinLength=1
65+
// +kubebuilder:validation:MaxLength=63
66+
// +kubebuilder:validation:Pattern=`^[a-zA-Z_][a-zA-Z0-9_]*$`
67+
Name string `json:"name"`
68+
69+
// URL is the MCP server's Streamable HTTP endpoint.
70+
// Mutually exclusive with OpenStackClientRef.
71+
// +kubebuilder:validation:Optional
72+
// +kubebuilder:validation:MinLength=1
73+
URL string `json:"url,omitempty"`
74+
75+
// OpenStackClientRef is the name of an OpenStackClient CR in the same
76+
// namespace that has MCP enabled. The controller auto-computes the
77+
// correct service URL and TLS CA configuration.
78+
// Mutually exclusive with URL.
79+
// +kubebuilder:validation:Optional
80+
// +kubebuilder:validation:MinLength=1
81+
OpenStackClientRef string `json:"openstackClientRef,omitempty"`
82+
}
2583

26-
// OpenStackAssistantSpec defines the desired state of OpenStackAssistant.
84+
// GooseConfig defines Goose-specific provider configuration
85+
type GooseConfig struct {
86+
// Model is the model identifier for the Goose AI agent
87+
// (e.g., "gemini/models/gemini-2.5-flash"). Sets the GOOSE_MODEL env var.
88+
// +kubebuilder:validation:Optional
89+
Model string `json:"model,omitempty"`
90+
91+
// Recipes is a ConfigMap name containing Goose recipe YAML files.
92+
// Each key in the ConfigMap becomes a recipe file registered as a
93+
// Goose slash command (e.g., /cluster-health).
94+
// +kubebuilder:validation:Optional
95+
Recipes *string `json:"recipes,omitempty"`
96+
97+
// Skills is a ConfigMap name containing Goose Agent Skill files.
98+
// Each key in the ConfigMap becomes a skill named after the key
99+
// (extension stripped), written as ~/.config/goose/skills/<name>/SKILL.md.
100+
// Unlike Recipes, skills are not explicitly invoked - Goose loads
101+
// them automatically when their description matches the task at hand.
102+
// +kubebuilder:validation:Optional
103+
Skills *string `json:"skills,omitempty"`
104+
105+
// Hints is a ConfigMap name containing Goose hints/context.
106+
// The ConfigMap must have a key "hints" with the content that
107+
// will be written to ~/.goosehints in the pod.
108+
// +kubebuilder:validation:Optional
109+
Hints *string `json:"hints,omitempty"`
110+
111+
// MCPServers lists MCP server endpoints to configure as Goose extensions.
112+
// +kubebuilder:validation:Optional
113+
MCPServers []MCPServerRef `json:"mcpServers,omitempty"`
114+
}
115+
116+
// OpenStackAssistantSpec defines the desired state of OpenStackAssistant
27117
type OpenStackAssistantSpec struct {
28-
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
29-
// Important: Run "make" to regenerate code after modifying this file
118+
// ContainerImage for the assistant container (will be set to environmental default if empty).
119+
// +kubebuilder:validation:Optional
120+
ContainerImage string `json:"containerImage,omitempty"`
121+
122+
// Provider is the AI agent provider type. Currently only "goose" is supported.
123+
// +kubebuilder:validation:Optional
124+
// +kubebuilder:default=goose
125+
Provider ProviderType `json:"provider,omitempty"`
126+
127+
// LightspeedStack configuration for the AI backend.
128+
// +kubebuilder:validation:Required
129+
LightspeedStack LightspeedStackSpec `json:"lightspeedStack"`
130+
131+
// Goose contains Goose-specific provider configuration.
132+
// Only applicable when provider is "goose".
133+
// +kubebuilder:validation:Optional
134+
Goose *GooseConfig `json:"goose,omitempty"`
135+
136+
// NodeSelector to target subset of worker nodes for pod scheduling.
137+
// +kubebuilder:validation:Optional
138+
NodeSelector *map[string]string `json:"nodeSelector,omitempty"`
30139

31-
// Foo is an example field of OpenStackAssistant. Edit openstackassistant_types.go to remove/update
32-
Foo string `json:"foo,omitempty"`
140+
// Env is a list of additional environment variables for the container.
141+
// +kubebuilder:validation:Optional
142+
// +listType=map
143+
// +listMapKey=name
144+
Env []corev1.EnvVar `json:"env,omitempty"`
33145
}
34146

35-
// OpenStackAssistantStatus defines the observed state of OpenStackAssistant.
147+
// OpenStackAssistantStatus defines the observed state of OpenStackAssistant
36148
type OpenStackAssistantStatus struct {
37-
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
38-
// Important: Run "make" to regenerate code after modifying this file
149+
// PodName is the name of the running assistant pod
150+
PodName string `json:"podName,omitempty"`
151+
152+
// Conditions tracks the state of each sub-resource
153+
Conditions condition.Conditions `json:"conditions,omitempty" optional:"true"`
154+
155+
// ObservedGeneration - the most recent generation observed
156+
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
157+
158+
// Hash tracks input hashes to detect changes
159+
Hash map[string]string `json:"hash,omitempty"`
39160
}
40161

41162
// +kubebuilder:object:root=true
42163
// +kubebuilder:subresource:status
164+
// +operator-sdk:csv:customresourcedefinitions:displayName="OpenStack Assistant"
165+
// +kubebuilder:resource:shortName=osassistant;osassistants
166+
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[0].status",description="Status"
167+
// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[0].message",description="Message"
43168

44-
// OpenStackAssistant is the Schema for the openstackassistants API.
169+
// OpenStackAssistant is the Schema for the openstackassistants API
45170
type OpenStackAssistant struct {
46171
metav1.TypeMeta `json:",inline"`
47172
metav1.ObjectMeta `json:"metadata,omitempty"`
@@ -52,7 +177,7 @@ type OpenStackAssistant struct {
52177

53178
// +kubebuilder:object:root=true
54179

55-
// OpenStackAssistantList contains a list of OpenStackAssistant.
180+
// OpenStackAssistantList contains a list of OpenStackAssistant
56181
type OpenStackAssistantList struct {
57182
metav1.TypeMeta `json:",inline"`
58183
metav1.ListMeta `json:"metadata,omitempty"`
@@ -62,3 +187,51 @@ type OpenStackAssistantList struct {
62187
func init() {
63188
SchemeBuilder.Register(&OpenStackAssistant{}, &OpenStackAssistantList{})
64189
}
190+
191+
// IsReady - returns true if OpenStackAssistant is reconciled successfully
192+
func (instance OpenStackAssistant) IsReady() bool {
193+
return instance.Status.Conditions.IsTrue(OpenStackAssistantReadyCondition)
194+
}
195+
196+
// RbacConditionsSet - set the conditions for the rbac object
197+
func (instance OpenStackAssistant) RbacConditionsSet(c *condition.Condition) {
198+
instance.Status.Conditions.Set(c)
199+
}
200+
201+
// RbacNamespace - return the namespace
202+
func (instance OpenStackAssistant) RbacNamespace() string {
203+
return instance.Namespace
204+
}
205+
206+
// RbacResourceName - return the name to be used for rbac objects (serviceaccount, role, rolebinding)
207+
func (instance OpenStackAssistant) RbacResourceName() string {
208+
return "openstackassistant-" + instance.Name
209+
}
210+
211+
// OpenStackAssistantDefaults holds defaults for the assistant
212+
type OpenStackAssistantDefaults struct {
213+
ContainerImageURL string
214+
}
215+
216+
var openStackAssistantDefaults OpenStackAssistantDefaults
217+
218+
// SetupOpenStackAssistantDefaults - initialize OpenStackAssistant spec defaults
219+
func SetupOpenStackAssistantDefaults(defaults OpenStackAssistantDefaults) {
220+
openStackAssistantDefaults = defaults
221+
}
222+
223+
// SetupDefaults - initializes any CRD field defaults based on environment variables
224+
func SetupDefaults() {
225+
openStackAssistantDefaults := OpenStackAssistantDefaults{
226+
ContainerImageURL: util.GetEnvVar("RELATED_IMAGE_OPENSTACK_ASSISTANT_IMAGE_URL_DEFAULT", OpenStackAssistantContainerImage),
227+
}
228+
229+
SetupOpenStackAssistantDefaults(openStackAssistantDefaults)
230+
}
231+
232+
// Default implements webhook.Defaulter
233+
func (r *OpenStackAssistant) Default() {
234+
if r.Spec.ContainerImage == "" {
235+
r.Spec.ContainerImage = openStackAssistantDefaults.ContainerImageURL
236+
}
237+
}
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)