Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cmd/thv-operator/api/v1alpha1/mcpregistry_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ type MCPRegistrySpec struct {
// Filter defines include/exclude patterns for registry content
// +optional
Filter *RegistryFilter `json:"filter,omitempty"`

// EnforceServers indicates whether MCPServers in this namespace must have their images
// present in at least one registry in the namespace. When any registry in the namespace
// has this field set to true, enforcement is enabled for the entire namespace.
// MCPServers with images not found in any registry will be rejected.
// When false (default), MCPServers can be deployed regardless of registry presence.
// +kubebuilder:default=false
// +optional
EnforceServers bool `json:"enforceServers,omitempty"`
}

// MCPRegistrySource defines the source configuration for registry data
Expand Down
17 changes: 17 additions & 0 deletions cmd/thv-operator/api/v1alpha1/mcpserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// Condition types for MCPServer
const (
// ConditionImageValidated indicates whether this image is fine to be used
ConditionImageValidated = "ImageValidated"
)

const (
// ConditionReasonImageValidationFailed indicates image validation failed
ConditionReasonImageValidationFailed = "ImageValidationFailed"
// ConditionReasonImageValidationSuccess indicates image validation succeeded
ConditionReasonImageValidationSuccess = "ImageValidationSuccess"
// ConditionReasonImageValidationError indicates an error occurred during validation
ConditionReasonImageValidationError = "ImageValidationError"
// ConditionReasonImageValidationSkipped indicates image validation was skipped
ConditionReasonImageValidationSkipped = "ImageValidationSkipped"
)

// MCPServerSpec defines the desired state of MCPServer
type MCPServerSpec struct {
// Image is the container image for the MCP server
Expand Down
56 changes: 56 additions & 0 deletions cmd/thv-operator/controllers/mcpserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package controllers
import (
"context"
"encoding/json"
goerr "errors"
"fmt"
"maps"
"os"
Expand All @@ -18,6 +19,7 @@ import (
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -30,6 +32,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"

mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/validation"
"github.com/stacklok/toolhive/pkg/container/kubernetes"
)

Expand All @@ -40,6 +43,7 @@ type MCPServerReconciler struct {
platformDetector kubernetes.PlatformDetector
detectedPlatform kubernetes.Platform
platformOnce sync.Once
ImageValidation validation.ImageValidation
}

// defaultRBACRules are the default RBAC rules that the
Expand Down Expand Up @@ -193,6 +197,47 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return ctrl.Result{}, err
}

// Validate MCPServer image against enforcing registries
imageValidator := validation.NewImageValidator(r.Client, mcpServer.Namespace, r.ImageValidation)
err = imageValidator.ValidateImage(ctx, mcpServer.Spec.Image, mcpServer.ObjectMeta)
if goerr.Is(err, validation.ErrImageNotChecked) {
ctxLogger.Info("Image validation skipped - no enforcement configured")
// Set condition to indicate validation was skipped
setImageValidationCondition(mcpServer, metav1.ConditionTrue,
mcpv1alpha1.ConditionReasonImageValidationSkipped,
"Image validation was not performed (no enforcement configured)")
} else if goerr.Is(err, validation.ErrImageInvalid) {
ctxLogger.Error(err, "MCPServer image validation failed", "image", mcpServer.Spec.Image)
// Update status to reflect validation failure
mcpServer.Status.Phase = mcpv1alpha1.MCPServerPhaseFailed
mcpServer.Status.Message = err.Error() // Gets the specific validation failure reason
setImageValidationCondition(mcpServer, metav1.ConditionFalse,
mcpv1alpha1.ConditionReasonImageValidationFailed,
err.Error()) // This will include the wrapped error context with specific reason
if statusErr := r.Status().Update(ctx, mcpServer); statusErr != nil {
ctxLogger.Error(statusErr, "Failed to update MCPServer status after validation error")
}
// Requeue after 5 minutes to retry validation
return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
} else if err != nil {
// Other system/infrastructure errors
ctxLogger.Error(err, "MCPServer image validation system error", "image", mcpServer.Spec.Image)
setImageValidationCondition(mcpServer, metav1.ConditionFalse,
mcpv1alpha1.ConditionReasonImageValidationError,
fmt.Sprintf("Error checking image validity: %v", err))
if statusErr := r.Status().Update(ctx, mcpServer); statusErr != nil {
ctxLogger.Error(statusErr, "Failed to update MCPServer status after validation error")
}
// Requeue after 5 minutes to retry validation
return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
} else {
// Validation passed
ctxLogger.Info("Image validation passed", "image", mcpServer.Spec.Image)
setImageValidationCondition(mcpServer, metav1.ConditionTrue,
mcpv1alpha1.ConditionReasonImageValidationSuccess,
"Image validation passed - image found in enforced registries")
}

// Check if the MCPServer instance is marked to be deleted
if mcpServer.GetDeletionTimestamp() != nil {
// The object is being deleted
Expand Down Expand Up @@ -350,6 +395,17 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return ctrl.Result{}, nil
}

// setImageValidationCondition is a helper function to set the image validation status condition
// This reduces code duplication in the image validation logic
func setImageValidationCondition(mcpServer *mcpv1alpha1.MCPServer, status metav1.ConditionStatus, reason, message string) {
meta.SetStatusCondition(&mcpServer.Status.Conditions, metav1.Condition{
Type: mcpv1alpha1.ConditionImageValidated,
Status: status,
Reason: reason,
Message: message,
})
}

// handleRestartAnnotation checks if the restart annotation has been updated and triggers a restart if needed
// Returns true if a restart was triggered and the reconciliation should be requeued
func (r *MCPServerReconciler) handleRestartAnnotation(ctx context.Context, mcpServer *mcpv1alpha1.MCPServer) (bool, error) {
Expand Down
14 changes: 10 additions & 4 deletions cmd/thv-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
"github.com/stacklok/toolhive/cmd/thv-operator/controllers"
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/validation"
"github.com/stacklok/toolhive/pkg/logger"
"github.com/stacklok/toolhive/pkg/operator/telemetry"
)
Expand Down Expand Up @@ -74,16 +75,21 @@ func main() {
os.Exit(1)
}

if err = (&controllers.MCPServerReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
rec := &controllers.MCPServerReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
ImageValidation: validation.ImageValidationAlwaysAllow,
}

if err = rec.SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "MCPServer")
os.Exit(1)
}

// Only register MCPRegistry controller if feature flag is enabled
if os.Getenv("ENABLE_EXPERIMENTAL_FEATURES") == "true" {
rec.ImageValidation = validation.ImageValidationRegistryEnforcing

if err = (controllers.NewMCPRegistryReconciler(mgr.GetClient(), mgr.GetScheme())).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "MCPRegistry")
os.Exit(1)
Expand Down
Loading
Loading