diff --git a/Makefile b/Makefile index 51c56627..1c392c9a 100644 --- a/Makefile +++ b/Makefile @@ -124,7 +124,7 @@ manifests: controller-gen yq ## šŸ“„ Generate WebhookConfiguration, ClusterRole @echo "āœ… RBAC manifests generated" @echo "\nšŸ“ Step 3: Generating object boilerplate..." - @$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths=./pkg/apis/ome/v1beta1 + @$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths=./pkg/apis/ome/... @echo "āœ… Object boilerplate generated" @echo "\nšŸ”„ Step 4: Applying CRD fixes and modifications..." diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 1fbd3acc..c2ce535a 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -37,6 +37,7 @@ import ( volcanobatch "volcano.sh/apis/pkg/apis/batch/v1alpha1" volcano "volcano.sh/apis/pkg/apis/scheduling/v1beta1" + "github.com/sgl-project/ome/pkg/apis/ome/v1alpha1" "github.com/sgl-project/ome/pkg/apis/ome/v1beta1" "github.com/sgl-project/ome/pkg/constants" v1beta1acceleratorclasscontroller "github.com/sgl-project/ome/pkg/controller/v1beta1/acceleratorclass" @@ -84,6 +85,7 @@ func init() { istionetworking.VirtualServiceUnmarshaler.AllowUnknownFields = true istionetworking.GatewayUnmarshaler.AllowUnknownFields = true + utilruntime.Must(v1alpha1.AddToScheme(scheme)) utilruntime.Must(v1beta1.AddToScheme(scheme)) utilruntime.Must(schedulerpluginsv1alpha1.AddToScheme(scheme)) utilruntime.Must(clientgoscheme.AddToScheme(scheme)) diff --git a/pkg/apis/ome/v1alpha1/doc.go b/pkg/apis/ome/v1alpha1/doc.go new file mode 100644 index 00000000..ca0778b9 --- /dev/null +++ b/pkg/apis/ome/v1alpha1/doc.go @@ -0,0 +1,6 @@ +// Package v1alpha1 contains API Schema definitions for the OME v1alpha1 API group +// +k8s:openapi-gen=true +// +kubebuilder:object:generate=true +// +k8s:defaulter-gen=TypeMeta +// +groupName=ome.io +package v1alpha1 diff --git a/pkg/apis/ome/v1alpha1/mcp_route_types.go b/pkg/apis/ome/v1alpha1/mcp_route_types.go new file mode 100644 index 00000000..79bd14f8 --- /dev/null +++ b/pkg/apis/ome/v1alpha1/mcp_route_types.go @@ -0,0 +1,318 @@ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// MCPRoute is the Schema for the mcproutes API +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=mcpr +// +kubebuilder:printcolumn:name="Backends",type=string,JSONPath=`.spec.backendRefs[*]` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` +type MCPRoute struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MCPRouteSpec `json:"spec"` + Status MCPRouteStatus `json:"status,omitempty"` +} + +type MCPRouteSpec struct { + // BackendRefs defines where to route requests + // All backends must be MCPServers in the same namespace + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + BackendRefs []MCPBackendRef `json:"backendRefs"` + + // Matches defines routing rules (optional) + // If not specified, routes all tools from backend servers + // +optional + Matches []MCPRouteMatch `json:"matches,omitempty"` + + // Authentication policy for this route + // Overrides/extends gateway default if present + // +optional + Authentication *MCPAuthentication `json:"authentication,omitempty"` + + // Authorization policy for this route + // Adds to gateway default authorization + // +optional + Authorization *MCPAuthorization `json:"authorization,omitempty"` + + // RateLimit for this route + // Adds to gateway default rate limits + // +optional + RateLimit *MCPRateLimit `json:"rateLimit,omitempty"` + + // Filters for request/response modification + // +optional + Filters []MCPRouteFilter `json:"filters,omitempty"` +} + +type MCPBackendRef struct { + // ServerRef references an MCPServer in the same namespace + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self.name != ''",message="ServerRef.name must be explicitly provided and cannot be empty" + ServerRef corev1.LocalObjectReference `json:"serverRef"` + + // Weight for traffic splitting across backends + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:default=1 + // +optional + Weight *int32 `json:"weight,omitempty"` +} + +type MCPRouteMatch struct { + // Tools to match - supports simple wildcards in tool names + // Examples: "db_query", "db_*", "*_query" + // +optional + Tools []string `json:"tools,omitempty"` + + // ToolMatch defines advanced tool matching (alternative to Tools) + // +optional + ToolMatch *ToolMatcher `json:"toolMatch,omitempty"` + + // Method to match (tools/call, tools/list, prompts/get, etc.) + // +optional + Method *string `json:"method,omitempty"` + + // Headers to match + // +optional + Headers []HeaderMatch `json:"headers,omitempty"` + + // BackendRefs for this match (optional) + // If specified, overrides route-level backendRefs for matching requests + // +optional + BackendRefs []MCPBackendRef `json:"backendRefs,omitempty"` +} + +type ToolMatcher struct { + // PrefixMatch matches tools with this prefix + // +optional + PrefixMatch *string `json:"prefixMatch,omitempty"` + + // ExactMatch matches exact tool names + // +optional + ExactMatch *string `json:"exactMatch,omitempty"` + + // RegexMatch matches tools using regex + // +optional + RegexMatch *string `json:"regexMatch,omitempty"` +} + +type HeaderMatch struct { + // Name of the header + Name string `json:"name"` + + // Value to match + Value string `json:"value"` + + // Type of match (Exact, Prefix, Regex) + // +kubebuilder:validation:Enum=Exact;Prefix;Regex + // +kubebuilder:default=Exact + Type string `json:"type"` +} + +type MCPRouteFilter struct { + // Type of filter + // +kubebuilder:validation:Enum=RequestHeaderModifier;ResponseHeaderModifier + Type string `json:"type"` + + // RequestHeaderModifier configuration + // +optional + RequestHeaderModifier *HeaderModifier `json:"requestHeaderModifier,omitempty"` + + // ResponseHeaderModifier configuration + // +optional + ResponseHeaderModifier *HeaderModifier `json:"responseHeaderModifier,omitempty"` +} + +type HeaderModifier struct { + // Set headers (replaces if exists) + // +optional + Set []Header `json:"set,omitempty"` + + // Add headers (appends if exists) + // +optional + Add []Header `json:"add,omitempty"` + + // Remove headers + // +optional + Remove []string `json:"remove,omitempty"` +} + +type Header struct { + Name string `json:"name"` + Value string `json:"value"` +} + +type MCPRouteStatus struct { + // Conditions represent the latest available observations + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // GatewayURL is the endpoint LLMs should connect to for this route + // Format: http://gateway.namespace/routes/{namespace}/{route-name} + GatewayURL string `json:"gatewayURL,omitempty"` + + // BackendStatuses shows health of each backend + BackendStatuses []BackendStatus `json:"backendStatuses,omitempty"` +} + +type BackendStatus struct { + // +kubebuilder:validation:XValidation:rule="self.name != ''",message="ServerRef.name must be explicitly provided and cannot be empty" + ServerRef corev1.LocalObjectReference `json:"serverRef"` + Ready bool `json:"ready"` + Endpoint string `json:"endpoint,omitempty"` + Message string `json:"message,omitempty"` +} + +type MCPAuthentication struct { + // OIDC defines OpenID Connect authentication + // +optional + OIDC *OIDCAuthentication `json:"oidc,omitempty"` + + // JWT defines JWT token authentication + // +optional + JWT *JWTAuthentication `json:"jwt,omitempty"` + + // APIKey defines API key authentication + // +optional + APIKey *APIKeyAuthentication `json:"apiKey,omitempty"` +} + +type OIDCAuthentication struct { + // Issuer is the OIDC issuer URL + // +kubebuilder:validation:Required + Issuer string `json:"issuer"` + + // ClientID is the OAuth2 client ID + // +kubebuilder:validation:Required + ClientID string `json:"clientID"` + + // ClientSecretRef references a Secret containing the client secret + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self.name != ''",message="clientSecretRef.name must be explicitly provided and cannot be empty" + ClientSecretRef corev1.SecretKeySelector `json:"clientSecretRef"` + + // Scopes defines the OAuth2 scopes to request + // +optional + Scopes []string `json:"scopes,omitempty"` +} + +type JWTAuthentication struct { + // Audiences defines valid JWT audiences + // +kubebuilder:validation:MinItems=1 + Audiences []string `json:"audiences"` + + // JWKSURI is the URI for the JSON Web Key Set + // +kubebuilder:validation:Required + JWKSURI string `json:"jwksURI"` + + // Issuer defines the expected JWT issuer (optional) + // +optional + Issuer *string `json:"issuer,omitempty"` +} + +type APIKeyAuthentication struct { + // Header is the name of the header containing the API key + // +kubebuilder:default="X-API-Key" + Header string `json:"header"` + + // SecretRefs references Secrets containing valid API keys + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:XValidation:rule="self.all(x, x.name != '')",message="secretRefs elements must have explicitly provided non-empty names" + SecretRefs []corev1.SecretKeySelector `json:"secretRefs"` +} + +type MCPAuthorization struct { + // Rules defines authorization rules + // +kubebuilder:validation:MinItems=1 + Rules []AuthorizationRule `json:"rules"` +} + +type AuthorizationRule struct { + // Principals this rule applies to (users, groups, service accounts) + // Format: "user:alice", "group:developers", "serviceaccount:my-sa" + // +kubebuilder:validation:MinItems=1 + Principals []string `json:"principals"` + + // Permissions define allowed actions + // +kubebuilder:validation:MinItems=1 + Permissions []Permission `json:"permissions"` + + // Conditions for additional filtering (optional) + // +optional + Conditions []Condition `json:"conditions,omitempty"` +} + +type Permission struct { + // Tools this permission applies to (supports wildcards) + // +kubebuilder:validation:MinItems=1 + Tools []string `json:"tools"` + + // Actions allowed + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:Enum=tools/call;tools/list + Actions []string `json:"actions"` +} + +type Condition struct { + // Type of condition check + // +kubebuilder:validation:Enum=IPAddress;TimeOfDay;RequestHeader + Type string `json:"type"` + + // Key for the condition (e.g., header name for RequestHeader type) + // +optional + Key *string `json:"key,omitempty"` + + // Value to match against + // +kubebuilder:validation:Required + Value string `json:"value"` + + // Operator for matching + // +kubebuilder:validation:Enum=Equal;NotEqual;In;NotIn;Matches;NotMatches + // +kubebuilder:default=Equal + Operator string `json:"operator"` +} + +type MCPRateLimit struct { + // Limits defines rate limiting rules + // +kubebuilder:validation:MinItems=1 + Limits []RateLimit `json:"limits"` +} + +type RateLimit struct { + // Dimension defines what to rate limit by + // +kubebuilder:validation:Enum=user;ip;tool;principal;namespace + // +kubebuilder:validation:Required + Dimension string `json:"dimension"` + + // Tools restricts this limit to specific tools (optional) + // +optional + Tools []string `json:"tools,omitempty"` + + // Requests is the maximum number of requests allowed + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Required + Requests int32 `json:"requests"` + + // Unit is the time unit for the limit + // +kubebuilder:validation:Enum=second;minute;hour;day + // +kubebuilder:validation:Required + Unit string `json:"unit"` +} + +// +kubebuilder:object:root=true + +// MCPRouteList contains a list of MCPRoute +type MCPRouteList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MCPRoute `json:"items"` +} + +func init() { + SchemeBuilder.Register(&MCPRoute{}, &MCPRouteList{}) +} diff --git a/pkg/apis/ome/v1alpha1/mcp_server_types.go b/pkg/apis/ome/v1alpha1/mcp_server_types.go new file mode 100644 index 00000000..758468b3 --- /dev/null +++ b/pkg/apis/ome/v1alpha1/mcp_server_types.go @@ -0,0 +1,157 @@ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// MCPTransportType defines the transport protocol for MCP communication. +// +kubebuilder:validation:Enum=stdio;streamable-http;sse +type MCPTransportType string + +const ( + TransportStdio MCPTransportType = "stdio" + TransportStreamableHTTP MCPTransportType = "streamable-http" + TransportSSE MCPTransportType = "sse" +) + +// MCPServerSpec defines the desired state of an MCPServer. +// An MCPServer can either be 'Hosted' within the cluster or a 'Remote' external service. +// +kubebuilder:validation:XValidation:rule="has(self.hosted) || has(self.remote)", message="either hosted or remote must be specified" +// +kubebuilder:validation:XValidation:rule="!(has(self.hosted) && has(self.remote))", message="hosted and remote are mutually exclusive" +type MCPServerSpec struct { + // Hosted defines a server that runs as pods within the cluster. + // +optional + Hosted *HostedMCPServer `json:"hosted,omitempty"` + + // Remote defines a server that is accessed via an external URL. + // +optional + Remote *RemoteMCPServer `json:"remote,omitempty"` + + // Transport specifies the transport protocol for MCP communication. + // +kubebuilder:default=stdio + // +optional + Transport MCPTransportType `json:"transport,omitempty"` + + // Capabilities defines the features supported by this server. + // +optional + Capabilities *MCPCapabilities `json:"capabilities,omitempty"` + + // Version of the MCP server software. + // +optional + Version string `json:"version,omitempty"` + + // PermissionProfile defines the operational permissions for the server. + // +optional + PermissionProfile *PermissionProfileSource `json:"permissionProfile,omitempty"` + + // ToolsFilter restricts the tools exposed by this server. + // +optional + // +listType=set + ToolsFilter []string `json:"toolsFilter,omitempty"` +} + +type HostedMCPServer struct { + // PodSpec defines the pod template to use for the MCP server. + PodSpec corev1.PodTemplateSpec `json:"podSpec"` + + // Replicas is the number of desired replicas for the server. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:default=1 + // +optional + Replicas *int32 `json:"replicas,omitempty"` +} + +type RemoteMCPServer struct { + // URL is the external URL of the remote MCP server. + // +kubebuilder:validation:Pattern=`^https?://.*` + URL string `json:"url"` +} + +type MCPCapabilities struct { + // Add capabilities here as needed + Tools bool `json:"tools,omitempty"` + Resources bool `json:"resources,omitempty"` + Prompts bool `json:"prompts,omitempty"` +} + +// +kubebuilder:validation:XValidation:rule="(has(self.builtin) ? 1 : 0) + (has(self.configMap) ? 1 : 0) + (has(self.inline) ? 1 : 0) <= 1",message="at most one of builtin, configMap, or inline can be set" +type PermissionProfileSource struct { + Builtin *BuiltinPermissionProfile `json:"builtin,omitempty"` + ConfigMap *corev1.ConfigMapKeySelector `json:"configMap,omitempty"` + Inline *PermissionProfileSpec `json:"inline,omitempty"` +} + +type BuiltinPermissionProfile string + +type PermissionProfileSpec struct { + // Allow specifies the permissions granted to the server. + // +listType=atomic + Allow []PermissionRule `json:"allow"` +} + +type PermissionRule struct { + KubeResources *KubeResourcePermission `json:"kubeResources,omitempty"` + Network *NetworkPermission `json:"network,omitempty"` +} + +type KubeResourcePermission struct { + APIGroups []string `json:"apiGroups"` + Resources []string `json:"resources"` + Verbs []string `json:"verbs"` + // Namespaces restricts permissions to specific namespaces + // If empty, permissions apply to the MCPServer's namespace only + // +optional + Namespaces []string `json:"namespaces,omitempty"` +} + +type NetworkPermission struct { + // AllowHost specifies allowed destination hosts + // Supports wildcards: "*.internal.svc.cluster.local" + // +optional + AllowHost []string `json:"allowHost,omitempty"` + + // AllowCIDR specifies allowed destination CIDR blocks (optional for future) + // +optional + AllowCIDR []string `json:"allowCIDR,omitempty"` +} + +type MCPServerStatus struct { + // Conditions represent the latest available observations + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // NetworkEnforcement indicates how network permissions are enforced + // +kubebuilder:validation:Enum=NetworkPolicy;ServiceMesh;None + // +optional + NetworkEnforcement *string `json:"networkEnforcement,omitempty"` + + // Ready indicates the server is ready to accept requests + Ready bool `json:"ready"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=mcps +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.ready` + +// MCPServer is the Schema for the mcpservers API +type MCPServer struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MCPServerSpec `json:"spec,omitempty"` + Status MCPServerStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// MCPServerList contains a list of MCPServer +type MCPServerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MCPServer `json:"items"` +} + +func init() { + SchemeBuilder.Register(&MCPServer{}, &MCPServerList{}) +} diff --git a/pkg/apis/ome/v1alpha1/v1alpha1.go b/pkg/apis/ome/v1alpha1/v1alpha1.go new file mode 100644 index 00000000..ebd070eb --- /dev/null +++ b/pkg/apis/ome/v1alpha1/v1alpha1.go @@ -0,0 +1,32 @@ +// Package v1alpha1 contains API Schema definitions for the OME v1alpha1 API group +// +k8s:openapi-gen=true +// +kubebuilder:object:generate=true +// +k8s:defaulter-gen=TypeMeta +// +groupName=ome.io +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" + + "github.com/sgl-project/ome/pkg/constants" +) + +var ( + // APIVersion is the current API version used to register these objects + APIVersion = "v1alpha1" + + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: constants.OMEAPIGroupName, Version: APIVersion} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} + + // AddToScheme is required by pkg/client/... + AddToScheme = SchemeBuilder.AddToScheme +) + +// Resource is required by pkg/client/listers/... +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +}