@@ -5,6 +5,7 @@ package controllers
5
5
import (
6
6
"context"
7
7
"encoding/json"
8
+ goerr "errors"
8
9
"fmt"
9
10
"maps"
10
11
"os"
@@ -18,6 +19,7 @@ import (
18
19
corev1 "k8s.io/api/core/v1"
19
20
rbacv1 "k8s.io/api/rbac/v1"
20
21
"k8s.io/apimachinery/pkg/api/errors"
22
+ "k8s.io/apimachinery/pkg/api/meta"
21
23
"k8s.io/apimachinery/pkg/api/resource"
22
24
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23
25
"k8s.io/apimachinery/pkg/runtime"
@@ -30,6 +32,7 @@ import (
30
32
"sigs.k8s.io/controller-runtime/pkg/log"
31
33
32
34
mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
35
+ "github.com/stacklok/toolhive/cmd/thv-operator/pkg/validation"
33
36
"github.com/stacklok/toolhive/pkg/container/kubernetes"
34
37
)
35
38
@@ -40,6 +43,7 @@ type MCPServerReconciler struct {
40
43
platformDetector kubernetes.PlatformDetector
41
44
detectedPlatform kubernetes.Platform
42
45
platformOnce sync.Once
46
+ ImageValidation validation.ImageValidation
43
47
}
44
48
45
49
// defaultRBACRules are the default RBAC rules that the
@@ -193,6 +197,47 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
193
197
return ctrl.Result {}, err
194
198
}
195
199
200
+ // Validate MCPServer image against enforcing registries
201
+ imageValidator := validation .NewImageValidator (r .Client , mcpServer .Namespace , r .ImageValidation )
202
+ err = imageValidator .ValidateImage (ctx , mcpServer .Spec .Image , mcpServer .ObjectMeta )
203
+ if goerr .Is (err , validation .ErrImageNotChecked ) {
204
+ ctxLogger .Info ("Image validation skipped - no enforcement configured" )
205
+ // Set condition to indicate validation was skipped
206
+ setImageValidationCondition (mcpServer , metav1 .ConditionTrue ,
207
+ mcpv1alpha1 .ConditionReasonImageValidationSkipped ,
208
+ "Image validation was not performed (no enforcement configured)" )
209
+ } else if goerr .Is (err , validation .ErrImageInvalid ) {
210
+ ctxLogger .Error (err , "MCPServer image validation failed" , "image" , mcpServer .Spec .Image )
211
+ // Update status to reflect validation failure
212
+ mcpServer .Status .Phase = mcpv1alpha1 .MCPServerPhaseFailed
213
+ mcpServer .Status .Message = err .Error () // Gets the specific validation failure reason
214
+ setImageValidationCondition (mcpServer , metav1 .ConditionFalse ,
215
+ mcpv1alpha1 .ConditionReasonImageValidationFailed ,
216
+ err .Error ()) // This will include the wrapped error context with specific reason
217
+ if statusErr := r .Status ().Update (ctx , mcpServer ); statusErr != nil {
218
+ ctxLogger .Error (statusErr , "Failed to update MCPServer status after validation error" )
219
+ }
220
+ // Requeue after 5 minutes to retry validation
221
+ return ctrl.Result {RequeueAfter : 5 * time .Minute }, nil
222
+ } else if err != nil {
223
+ // Other system/infrastructure errors
224
+ ctxLogger .Error (err , "MCPServer image validation system error" , "image" , mcpServer .Spec .Image )
225
+ setImageValidationCondition (mcpServer , metav1 .ConditionFalse ,
226
+ mcpv1alpha1 .ConditionReasonImageValidationError ,
227
+ fmt .Sprintf ("Error checking image validity: %v" , err ))
228
+ if statusErr := r .Status ().Update (ctx , mcpServer ); statusErr != nil {
229
+ ctxLogger .Error (statusErr , "Failed to update MCPServer status after validation error" )
230
+ }
231
+ // Requeue after 5 minutes to retry validation
232
+ return ctrl.Result {RequeueAfter : 5 * time .Minute }, nil
233
+ } else {
234
+ // Validation passed
235
+ ctxLogger .Info ("Image validation passed" , "image" , mcpServer .Spec .Image )
236
+ setImageValidationCondition (mcpServer , metav1 .ConditionTrue ,
237
+ mcpv1alpha1 .ConditionReasonImageValidationSuccess ,
238
+ "Image validation passed - image found in enforced registries" )
239
+ }
240
+
196
241
// Check if the MCPServer instance is marked to be deleted
197
242
if mcpServer .GetDeletionTimestamp () != nil {
198
243
// The object is being deleted
@@ -350,6 +395,17 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
350
395
return ctrl.Result {}, nil
351
396
}
352
397
398
+ // setImageValidationCondition is a helper function to set the image validation status condition
399
+ // This reduces code duplication in the image validation logic
400
+ func setImageValidationCondition (mcpServer * mcpv1alpha1.MCPServer , status metav1.ConditionStatus , reason , message string ) {
401
+ meta .SetStatusCondition (& mcpServer .Status .Conditions , metav1.Condition {
402
+ Type : mcpv1alpha1 .ConditionImageValidated ,
403
+ Status : status ,
404
+ Reason : reason ,
405
+ Message : message ,
406
+ })
407
+ }
408
+
353
409
// handleRestartAnnotation checks if the restart annotation has been updated and triggers a restart if needed
354
410
// Returns true if a restart was triggered and the reconciliation should be requeued
355
411
func (r * MCPServerReconciler ) handleRestartAnnotation (ctx context.Context , mcpServer * mcpv1alpha1.MCPServer ) (bool , error ) {
0 commit comments