@@ -5,6 +5,7 @@ package controllers
55import (
66 "context"
77 "encoding/json"
8+ goerr "errors"
89 "fmt"
910 "maps"
1011 "os"
@@ -18,6 +19,7 @@ import (
1819 corev1 "k8s.io/api/core/v1"
1920 rbacv1 "k8s.io/api/rbac/v1"
2021 "k8s.io/apimachinery/pkg/api/errors"
22+ "k8s.io/apimachinery/pkg/api/meta"
2123 "k8s.io/apimachinery/pkg/api/resource"
2224 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2325 "k8s.io/apimachinery/pkg/runtime"
@@ -30,6 +32,7 @@ import (
3032 "sigs.k8s.io/controller-runtime/pkg/log"
3133
3234 mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
35+ "github.com/stacklok/toolhive/cmd/thv-operator/pkg/validation"
3336 "github.com/stacklok/toolhive/pkg/container/kubernetes"
3437)
3538
@@ -40,6 +43,7 @@ type MCPServerReconciler struct {
4043 platformDetector kubernetes.PlatformDetector
4144 detectedPlatform kubernetes.Platform
4245 platformOnce sync.Once
46+ ImageValidation validation.ImageValidation
4347}
4448
4549// defaultRBACRules are the default RBAC rules that the
@@ -193,6 +197,47 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
193197 return ctrl.Result {}, err
194198 }
195199
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+
196241 // Check if the MCPServer instance is marked to be deleted
197242 if mcpServer .GetDeletionTimestamp () != nil {
198243 // The object is being deleted
@@ -350,6 +395,17 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
350395 return ctrl.Result {}, nil
351396}
352397
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+
353409// handleRestartAnnotation checks if the restart annotation has been updated and triggers a restart if needed
354410// Returns true if a restart was triggered and the reconciliation should be requeued
355411func (r * MCPServerReconciler ) handleRestartAnnotation (ctx context.Context , mcpServer * mcpv1alpha1.MCPServer ) (bool , error ) {
0 commit comments