Skip to content

Commit e940550

Browse files
committed
Webhook Middleware Phase 5: Kubernetes CRD and controller integration
Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
1 parent f94b99b commit e940550

31 files changed

Lines changed: 4110 additions & 9 deletions

cmd/thv-operator/api/v1alpha1/types.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,34 @@ type MCPTelemetryConfigList struct {
276276
Items []MCPTelemetryConfig `json:"items"`
277277
}
278278

279+
// ─── MCPWebhookConfig ────────────────────────────────────────────────────────
280+
281+
//+kubebuilder:object:root=true
282+
//+kubebuilder:deprecatedversion:warning="toolhive.stacklok.dev/v1alpha1 is deprecated; use v1beta1"
283+
//+kubebuilder:subresource:status
284+
//+kubebuilder:resource:shortName=mwc,categories=toolhive
285+
//+kubebuilder:printcolumn:name="Valid",type=string,JSONPath=`.status.conditions[?(@.type=='Valid')].status`
286+
//+kubebuilder:printcolumn:name="References",type=string,JSONPath=`.status.referencingWorkloads`
287+
//+kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
288+
289+
// MCPWebhookConfig is the deprecated v1alpha1 version of the MCPWebhookConfig resource.
290+
type MCPWebhookConfig struct {
291+
metav1.TypeMeta `json:",inline"` // nolint:revive
292+
metav1.ObjectMeta `json:"metadata,omitempty"`
293+
294+
Spec v1beta1.MCPWebhookConfigSpec `json:"spec,omitempty"`
295+
Status v1beta1.MCPWebhookConfigStatus `json:"status,omitempty"`
296+
}
297+
298+
//+kubebuilder:object:root=true
299+
300+
// MCPWebhookConfigList contains a list of MCPWebhookConfig.
301+
type MCPWebhookConfigList struct {
302+
metav1.TypeMeta `json:",inline"` // nolint:revive
303+
metav1.ListMeta `json:"metadata,omitempty"`
304+
Items []MCPWebhookConfig `json:"items"`
305+
}
306+
279307
// ─── MCPToolConfig ───────────────────────────────────────────────────────────
280308

281309
//+kubebuilder:object:root=true
@@ -378,6 +406,7 @@ func init() {
378406
&MCPServer{}, &MCPServerList{},
379407
&MCPServerEntry{}, &MCPServerEntryList{},
380408
&MCPTelemetryConfig{}, &MCPTelemetryConfigList{},
409+
&MCPWebhookConfig{}, &MCPWebhookConfigList{},
381410
&MCPToolConfig{}, &MCPToolConfigList{},
382411
&VirtualMCPCompositeToolDefinition{}, &VirtualMCPCompositeToolDefinitionList{},
383412
&VirtualMCPServer{}, &VirtualMCPServerList{},

cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 59 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/thv-operator/api/v1beta1/mcpserver_types.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,18 @@ const (
8686
const (
8787
// ConditionTypeExternalAuthConfigValidated indicates whether the ExternalAuthConfig is valid
8888
ConditionTypeExternalAuthConfigValidated = "ExternalAuthConfigValidated"
89+
90+
// ConditionTypeWebhookConfigValidated indicates whether the WebhookConfig is valid
91+
ConditionTypeWebhookConfigValidated = "WebhookConfigValidated"
8992
)
9093

9194
const (
9295
// ConditionReasonExternalAuthConfigMultiUpstream indicates the ExternalAuthConfig has multiple upstreams,
9396
// which is not supported for MCPServer (use VirtualMCPServer for multi-upstream).
9497
ConditionReasonExternalAuthConfigMultiUpstream = "MultiUpstreamNotSupported"
98+
99+
// ConditionReasonWebhookConfigInvalid indicates the referenced webhook config is invalid or missing
100+
ConditionReasonWebhookConfigInvalid = "WebhookConfigInvalid"
95101
)
96102

97103
const (
@@ -287,6 +293,11 @@ type MCPServerSpec struct {
287293
// +optional
288294
ExternalAuthConfigRef *ExternalAuthConfigRef `json:"externalAuthConfigRef,omitempty"`
289295

296+
// WebhookConfigRef references a MCPWebhookConfig resource for webhook middleware configuration.
297+
// The referenced MCPWebhookConfig must exist in the same namespace as this MCPServer.
298+
// +optional
299+
WebhookConfigRef *WebhookConfigRef `json:"webhookConfigRef,omitempty"`
300+
290301
// AuthServerRef optionally references a resource that configures an embedded
291302
// OAuth 2.0/OIDC authorization server to authenticate MCP clients.
292303
// Currently the only supported kind is MCPExternalAuthConfig (type: embeddedAuthServer).
@@ -717,6 +728,14 @@ type AuthServerRef struct {
717728
Name string `json:"name"`
718729
}
719730

731+
// WebhookConfigRef defines a reference to a MCPWebhookConfig resource.
732+
// The referenced MCPWebhookConfig must be in the same namespace as the MCPServer.
733+
type WebhookConfigRef struct {
734+
// Name is the name of the MCPWebhookConfig resource
735+
// +kubebuilder:validation:Required
736+
Name string `json:"name"`
737+
}
738+
720739
// ToolConfigRef defines a reference to a MCPToolConfig resource.
721740
// The referenced MCPToolConfig must be in the same namespace as the MCPServer.
722741
type ToolConfigRef struct {
@@ -828,6 +847,10 @@ type MCPServerStatus struct {
828847
// +optional
829848
TelemetryConfigHash string `json:"telemetryConfigHash,omitempty"`
830849

850+
// WebhookConfigHash is the hash of the referenced MCPWebhookConfig spec
851+
// +optional
852+
WebhookConfigHash string `json:"webhookConfigHash,omitempty"`
853+
831854
// URL is the URL where the MCP server can be accessed
832855
// +optional
833856
URL string `json:"url,omitempty"`
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package v1beta1
5+
6+
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
8+
// WebhookFailurePolicy defines how webhook errors are handled.
9+
type WebhookFailurePolicy string
10+
11+
const (
12+
// WebhookFailurePolicyFail denies the request on webhook error.
13+
WebhookFailurePolicyFail WebhookFailurePolicy = "fail"
14+
// WebhookFailurePolicyIgnore allows the request on webhook error.
15+
WebhookFailurePolicyIgnore WebhookFailurePolicy = "ignore"
16+
)
17+
18+
// WebhookTLSConfig contains TLS configuration for secure webhook connections
19+
type WebhookTLSConfig struct {
20+
// CASecretRef references a Secret containing the CA certificate bundle used to verify the webhook server's certificate.
21+
// Contains a bundle of PEM-encoded X.509 certificates.
22+
// +optional
23+
CASecretRef *SecretKeyRef `json:"caSecretRef,omitempty"`
24+
25+
// ClientCertSecretRef references a Secret containing the client certificate for mTLS authentication.
26+
// The referenced key must contain a PEM-encoded client certificate.
27+
// Use ClientKeySecretRef to provide the corresponding private key.
28+
// +optional
29+
ClientCertSecretRef *SecretKeyRef `json:"clientCertSecretRef,omitempty"`
30+
31+
// ClientKeySecretRef references a Secret containing the private key for the client certificate.
32+
// Required when ClientCertSecretRef is set to enable mTLS.
33+
// +optional
34+
ClientKeySecretRef *SecretKeyRef `json:"clientKeySecretRef,omitempty"`
35+
36+
// InsecureSkipVerify disables server certificate verification.
37+
// WARNING: This should only be used for development/testing and not in production environments.
38+
// +optional
39+
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
40+
}
41+
42+
// WebhookSpec defines the configuration for a single webhook middleware
43+
type WebhookSpec struct {
44+
// Name is a unique identifier for this webhook
45+
// +kubebuilder:validation:MinLength=1
46+
// +kubebuilder:validation:MaxLength=63
47+
Name string `json:"name"`
48+
49+
// URL is the endpoint to call for this webhook. Must be an HTTP/HTTPS URL.
50+
// +kubebuilder:validation:Format=uri
51+
URL string `json:"url"`
52+
53+
// Timeout configures the maximum time to wait for the webhook to respond.
54+
// Defaults to 10s if not specified. Maximum is 30s.
55+
// +kubebuilder:validation:Type=string
56+
// +kubebuilder:validation:Format=duration
57+
// +optional
58+
Timeout *metav1.Duration `json:"timeout,omitempty"`
59+
60+
// FailurePolicy defines how to handle errors when communicating with the webhook.
61+
// Supported values: "fail", "ignore". Defaults to "fail".
62+
// +kubebuilder:validation:Enum=fail;ignore
63+
// +kubebuilder:default=fail
64+
// +optional
65+
FailurePolicy WebhookFailurePolicy `json:"failurePolicy,omitempty"`
66+
67+
// TLSConfig contains optional TLS configuration for the webhook connection.
68+
// +optional
69+
TLSConfig *WebhookTLSConfig `json:"tlsConfig,omitempty"`
70+
71+
// HMACSecretRef references a Kubernetes Secret containing the HMAC signing key
72+
// used to sign the webhook payload. If set, the X-Toolhive-Signature header will be injected.
73+
// +optional
74+
HMACSecretRef *SecretKeyRef `json:"hmacSecretRef,omitempty"`
75+
}
76+
77+
// MCPWebhookConfigSpec defines the desired state of MCPWebhookConfig
78+
// +kubebuilder:validation:XValidation:rule="(has(self.validating) ? size(self.validating) : 0) + (has(self.mutating) ? size(self.mutating) : 0) > 0",message="at least one validating or mutating webhook must be defined"
79+
//
80+
//nolint:lll // CEL validation rules exceed line length limit
81+
type MCPWebhookConfigSpec struct {
82+
// Validating webhooks are called to approve or deny MCP requests.
83+
// +optional
84+
Validating []WebhookSpec `json:"validating,omitempty"`
85+
86+
// Mutating webhooks are called to transform MCP requests before processing.
87+
// +optional
88+
Mutating []WebhookSpec `json:"mutating,omitempty"`
89+
}
90+
91+
// MCPWebhookConfigStatus defines the observed state of MCPWebhookConfig
92+
type MCPWebhookConfigStatus struct {
93+
// Conditions represent the latest available observations
94+
// +listType=map
95+
// +listMapKey=type
96+
// +optional
97+
Conditions []metav1.Condition `json:"conditions,omitempty"`
98+
99+
// ObservedGeneration is the last observed generation corresponding to the current status
100+
// +optional
101+
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
102+
103+
// ConfigHash is a hash of the spec, used for detecting changes
104+
// +optional
105+
ConfigHash string `json:"configHash,omitempty"`
106+
107+
// ReferencingWorkloads is a list of workload resources that reference this MCPWebhookConfig.
108+
// Each entry identifies the workload by kind and name.
109+
// +listType=map
110+
// +listMapKey=name
111+
// +optional
112+
ReferencingWorkloads []WorkloadReference `json:"referencingWorkloads,omitempty"`
113+
}
114+
115+
// MCPWebhookConfig is the Schema for the mcpwebhookconfigs API.
116+
//
117+
// +kubebuilder:object:root=true
118+
// +kubebuilder:storageversion
119+
// +kubebuilder:subresource:status
120+
// +kubebuilder:resource:shortName=mwc,categories=toolhive
121+
// +kubebuilder:printcolumn:name="Valid",type=string,JSONPath=`.status.conditions[?(@.type=='Valid')].status`
122+
// +kubebuilder:printcolumn:name="References",type=string,JSONPath=`.status.referencingWorkloads`
123+
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
124+
//
125+
//nolint:lll // kubebuilder markers exceed line length limit
126+
type MCPWebhookConfig struct {
127+
metav1.TypeMeta `json:",inline"` //nolint:revive
128+
metav1.ObjectMeta `json:"metadata,omitempty"`
129+
130+
Spec MCPWebhookConfigSpec `json:"spec,omitempty"`
131+
Status MCPWebhookConfigStatus `json:"status,omitempty"`
132+
}
133+
134+
// +kubebuilder:object:root=true
135+
136+
// MCPWebhookConfigList contains a list of MCPWebhookConfig
137+
type MCPWebhookConfigList struct {
138+
metav1.TypeMeta `json:",inline"` //nolint:revive
139+
metav1.ListMeta `json:"metadata,omitempty"`
140+
Items []MCPWebhookConfig `json:"items"`
141+
}
142+
143+
func init() {
144+
SchemeBuilder.Register(&MCPWebhookConfig{}, &MCPWebhookConfigList{})
145+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package v1beta1
5+
6+
import (
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/assert"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
)
13+
14+
func TestMCPWebhookConfig_Creation(t *testing.T) {
15+
t.Parallel()
16+
17+
timeout := metav1.Duration{Duration: 5 * time.Second}
18+
19+
config := &MCPWebhookConfig{
20+
ObjectMeta: metav1.ObjectMeta{
21+
Name: "test-webhook-config",
22+
Namespace: "default",
23+
},
24+
Spec: MCPWebhookConfigSpec{
25+
Validating: []WebhookSpec{
26+
{
27+
Name: "test-validator",
28+
URL: "https://example.com/validate",
29+
Timeout: &timeout,
30+
FailurePolicy: WebhookFailurePolicyFail,
31+
TLSConfig: &WebhookTLSConfig{
32+
InsecureSkipVerify: true,
33+
},
34+
},
35+
},
36+
Mutating: []WebhookSpec{
37+
{
38+
Name: "test-mutator",
39+
URL: "https://example.com/mutate",
40+
Timeout: &timeout,
41+
FailurePolicy: WebhookFailurePolicyIgnore,
42+
HMACSecretRef: &SecretKeyRef{
43+
Name: "hmac-secret",
44+
Key: "key",
45+
},
46+
},
47+
},
48+
},
49+
}
50+
51+
assert.NotNil(t, config)
52+
assert.Equal(t, "test-webhook-config", config.Name)
53+
assert.Len(t, config.Spec.Validating, 1)
54+
assert.Len(t, config.Spec.Mutating, 1)
55+
56+
assert.Equal(t, "test-validator", config.Spec.Validating[0].Name)
57+
assert.Equal(t, WebhookFailurePolicyFail, config.Spec.Validating[0].FailurePolicy)
58+
assert.True(t, config.Spec.Validating[0].TLSConfig.InsecureSkipVerify)
59+
60+
assert.Equal(t, "test-mutator", config.Spec.Mutating[0].Name)
61+
assert.Equal(t, WebhookFailurePolicyIgnore, config.Spec.Mutating[0].FailurePolicy)
62+
assert.Equal(t, "hmac-secret", config.Spec.Mutating[0].HMACSecretRef.Name)
63+
}

0 commit comments

Comments
 (0)