Skip to content

Commit d7cb3fc

Browse files
committed
feat(controller): create prerequisites from annotation
When the annotation mcp.x-k8s.io/auto-create-prerequisites is set to "true", the operator creates missing ServiceAccounts and ConfigMaps referenced by the MCPServer CR before validation. Resources are owned by the MCPServer so they are cleaned up on deletion. Fixes: #226
1 parent b7521b0 commit d7cb3fc

4 files changed

Lines changed: 564 additions & 2 deletions

File tree

config/rbac/role.yaml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ rules:
88
- ""
99
resources:
1010
- configmaps
11-
- secrets
1211
verbs:
12+
- create
1313
- get
1414
- list
1515
- watch
@@ -20,6 +20,21 @@ rules:
2020
verbs:
2121
- get
2222
- list
23+
- apiGroups:
24+
- ""
25+
resources:
26+
- secrets
27+
verbs:
28+
- get
29+
- list
30+
- watch
31+
- apiGroups:
32+
- ""
33+
resources:
34+
- serviceaccounts
35+
verbs:
36+
- create
37+
- get
2338
- apiGroups:
2439
- ""
2540
resources:

internal/controller/mcpserver_controller.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,9 @@ type MCPServerReconciler struct {
164164
// +kubebuilder:rbac:groups=mcp.x-k8s.io,resources=mcpservers/finalizers,verbs=update
165165
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
166166
// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
167-
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch
167+
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create
168168
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
169+
// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;create
169170
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list
170171
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
171172
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
@@ -195,6 +196,12 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
195196
pendingAcceptedEvent := !acceptedConditionIsTrue(mcpServer.Status.Conditions)
196197
pendingServerReadyEvent := !readyConditionIsAvailable(mcpServer.Status.Conditions)
197198

199+
// Before validation, create prerequisites if annotation is set
200+
if err := r.ensurePrerequisites(ctx, mcpServer); err != nil {
201+
logger.Error(err, "Failed to create prerequisites")
202+
// Don't return error — fall through to validation which will report the specific missing resource
203+
}
204+
198205
// Validate configuration
199206
validationStart := time.Now()
200207
if err := r.validateConfig(ctx, mcpServer); err != nil {
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
Copyright 2026 The Kubernetes Authors
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 controller
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
corev1 "k8s.io/api/core/v1"
24+
apierrors "k8s.io/apimachinery/pkg/api/errors"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
28+
"sigs.k8s.io/controller-runtime/pkg/log"
29+
30+
mcpv1alpha1 "github.com/kubernetes-sigs/mcp-lifecycle-operator/api/v1alpha1"
31+
)
32+
33+
const (
34+
// AnnotationAutoCreatePrerequisites controls whether the operator creates
35+
// missing ConfigMaps and ServiceAccounts referenced by the MCPServer CR.
36+
AnnotationAutoCreatePrerequisites = "mcp.x-k8s.io/auto-create-prerequisites"
37+
38+
// eventActionPrerequisiteCreated is the reporting action when a prerequisite resource is auto-created.
39+
eventActionPrerequisiteCreated = "PrerequisiteCreated"
40+
)
41+
42+
// ensurePrerequisites creates missing ServiceAccounts and ConfigMaps referenced
43+
// by the MCPServer when the auto-create-prerequisites annotation is set to "true".
44+
// Created resources are owned by the MCPServer so they are cleaned up on deletion.
45+
func (r *MCPServerReconciler) ensurePrerequisites(ctx context.Context, server *mcpv1alpha1.MCPServer) error {
46+
if server.Annotations[AnnotationAutoCreatePrerequisites] != "true" {
47+
return nil
48+
}
49+
50+
logger := log.FromContext(ctx)
51+
logger.Info("Auto-creating prerequisites for MCPServer", "name", server.Name)
52+
53+
if err := r.ensureServiceAccount(ctx, server); err != nil {
54+
return err
55+
}
56+
57+
if err := r.ensureConfigMaps(ctx, server); err != nil {
58+
return err
59+
}
60+
61+
return nil
62+
}
63+
64+
// ensureServiceAccount creates the ServiceAccount referenced by
65+
// spec.runtime.security.serviceAccountName if it does not already exist.
66+
func (r *MCPServerReconciler) ensureServiceAccount(ctx context.Context, server *mcpv1alpha1.MCPServer) error {
67+
saName := server.Spec.Runtime.Security.ServiceAccountName
68+
if saName == "" {
69+
return nil
70+
}
71+
72+
sa := &corev1.ServiceAccount{}
73+
err := r.Get(ctx, client.ObjectKey{Name: saName, Namespace: server.Namespace}, sa)
74+
if err == nil {
75+
// Already exists, nothing to do.
76+
return nil
77+
}
78+
if !apierrors.IsNotFound(err) {
79+
return fmt.Errorf("failed to check ServiceAccount %s: %w", saName, err)
80+
}
81+
82+
sa = &corev1.ServiceAccount{
83+
ObjectMeta: metav1.ObjectMeta{
84+
Name: saName,
85+
Namespace: server.Namespace,
86+
},
87+
}
88+
if err := controllerutil.SetControllerReference(server, sa, r.Scheme); err != nil {
89+
return fmt.Errorf("failed to set owner reference on ServiceAccount %s: %w", saName, err)
90+
}
91+
if err := r.Create(ctx, sa); err != nil {
92+
if apierrors.IsAlreadyExists(err) {
93+
return nil
94+
}
95+
return fmt.Errorf("failed to create ServiceAccount %s: %w", saName, err)
96+
}
97+
98+
r.emitPrerequisiteCreated(server, "ServiceAccount", saName)
99+
return nil
100+
}
101+
102+
// ensureConfigMaps creates any ConfigMaps referenced in spec.config.storage
103+
// that do not already exist.
104+
func (r *MCPServerReconciler) ensureConfigMaps(ctx context.Context, server *mcpv1alpha1.MCPServer) error {
105+
for i, storage := range server.Spec.Config.Storage {
106+
if storage.Source.Type != mcpv1alpha1.StorageTypeConfigMap {
107+
continue
108+
}
109+
if storage.Source.ConfigMap == nil {
110+
continue
111+
}
112+
113+
cmName := storage.Source.ConfigMap.Name
114+
if cmName == "" {
115+
continue
116+
}
117+
118+
cm := &corev1.ConfigMap{}
119+
err := r.Get(ctx, client.ObjectKey{Name: cmName, Namespace: server.Namespace}, cm)
120+
if err == nil {
121+
// Already exists, nothing to do.
122+
continue
123+
}
124+
if !apierrors.IsNotFound(err) {
125+
return fmt.Errorf("failed to check ConfigMap %s: %w", cmName, err)
126+
}
127+
128+
cm = &corev1.ConfigMap{
129+
ObjectMeta: metav1.ObjectMeta{
130+
Name: cmName,
131+
Namespace: server.Namespace,
132+
},
133+
Data: map[string]string{},
134+
}
135+
if err := controllerutil.SetControllerReference(server, cm, r.Scheme); err != nil {
136+
return fmt.Errorf("failed to set owner reference on ConfigMap %s: %w", cmName, err)
137+
}
138+
if err := r.Create(ctx, cm); err != nil {
139+
if apierrors.IsAlreadyExists(err) {
140+
continue
141+
}
142+
return fmt.Errorf("failed to create ConfigMap %s at storage index %d: %w", cmName, i, err)
143+
}
144+
145+
r.emitPrerequisiteCreated(server, "ConfigMap", cmName)
146+
}
147+
148+
return nil
149+
}
150+
151+
func (r *MCPServerReconciler) emitPrerequisiteCreated(server *mcpv1alpha1.MCPServer, kind, name string) {
152+
if r.Recorder == nil {
153+
return
154+
}
155+
r.Recorder.Eventf(server, nil, corev1.EventTypeNormal, "CreatedPrerequisite",
156+
eventActionPrerequisiteCreated, "Created %s %s for MCPServer %s", kind, name, server.Name)
157+
}

0 commit comments

Comments
 (0)