Skip to content

Commit fe03aa7

Browse files
Copilotg3force
andauthored
Add ToolGateway reference support to ToolServer (#67)
* Initial plan * Add ToolGateway reference support to ToolServer CRD Co-authored-by: g3force <779094+g3force@users.noreply.github.com> * Fix tests: add required Protocol field to ToolServer specs Co-authored-by: g3force <779094+g3force@users.noreply.github.com> * Address code review: use defaultToolGatewayNamespace constant in tests Co-authored-by: g3force <779094+g3force@users.noreply.github.com> * Fix test cleanup to prevent cross-test pollution The tests were failing in CI because other test files (specifically agent_tool_test.go) create ToolServers in various namespaces that weren't being cleaned up between tests. This caused the findToolServersReferencingToolGateway tests to find more ToolServers than expected. Changed the AfterEach cleanup to delete ALL ToolServers and ToolGateways across all namespaces, rather than just specific namespaces, to ensure complete isolation between tests. Co-authored-by: g3force <779094+g3force@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: g3force <779094+g3force@users.noreply.github.com>
1 parent 68d7f3b commit fe03aa7

7 files changed

Lines changed: 820 additions & 8 deletions

File tree

api/v1alpha1/toolserver_types.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ type ToolServerSpec struct {
8282
// Resources defines the compute resource requirements for the tool server container.
8383
// +optional
8484
Resources *corev1.ResourceRequirements `json:"resources,omitempty"`
85+
86+
// ToolGatewayRef references a ToolGateway resource that this tool server should use for routing.
87+
// If not specified, the operator will attempt to find the default ToolGateway in the cluster.
88+
// If no default ToolGateway exists, the tool server will run without a Tool Gateway.
89+
// If Namespace is not specified, defaults to the same namespace as the ToolServer.
90+
// +optional
91+
ToolGatewayRef *corev1.ObjectReference `json:"toolGatewayRef,omitempty"`
8592
}
8693

8794
// ToolServerStatus defines the observed state of ToolServer.
@@ -93,12 +100,19 @@ type ToolServerStatus struct {
93100
// Format: http://{name}.{namespace}.svc.cluster.local:{port}{path}
94101
// +optional
95102
Url string `json:"url,omitempty"`
103+
104+
// ToolGatewayRef references the ToolGateway resource that this tool server is connected to.
105+
// This field is automatically populated by the controller when a Tool Gateway is being used.
106+
// If nil, the tool server is not connected to any Tool Gateway.
107+
// +optional
108+
ToolGatewayRef *corev1.ObjectReference `json:"toolGatewayRef,omitempty"`
96109
}
97110

98111
// ToolServer is the Schema for the toolservers API.
99112
//
100113
// +kubebuilder:object:root=true
101114
// +kubebuilder:subresource:status
115+
// +kubebuilder:printcolumn:name="Tool Gateway",type=string,JSONPath=".status.toolGatewayRef.name"
102116
type ToolServer struct {
103117
metav1.TypeMeta `json:",inline"`
104118
metav1.ObjectMeta `json:"metadata,omitempty"`

api/v1alpha1/zz_generated.deepcopy.go

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

config/crd/bases/runtime.agentic-layer.ai_toolservers.yaml

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ spec:
1414
singular: toolserver
1515
scope: Namespaced
1616
versions:
17-
- name: v1alpha1
17+
- additionalPrinterColumns:
18+
- jsonPath: .status.toolGatewayRef.name
19+
name: Tool Gateway
20+
type: string
21+
name: v1alpha1
1822
schema:
1923
openAPIV3Schema:
2024
description: ToolServer is the Schema for the toolservers API.
@@ -354,6 +358,53 @@ spec:
354358
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
355359
type: object
356360
type: object
361+
toolGatewayRef:
362+
description: |-
363+
ToolGatewayRef references a ToolGateway resource that this tool server should use for routing.
364+
If not specified, the operator will attempt to find the default ToolGateway in the cluster.
365+
If no default ToolGateway exists, the tool server will run without a Tool Gateway.
366+
If Namespace is not specified, defaults to the same namespace as the ToolServer.
367+
properties:
368+
apiVersion:
369+
description: API version of the referent.
370+
type: string
371+
fieldPath:
372+
description: |-
373+
If referring to a piece of an object instead of an entire object, this string
374+
should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2].
375+
For example, if the object reference is to a container within a pod, this would take on a value like:
376+
"spec.containers{name}" (where "name" refers to the name of the container that triggered
377+
the event) or if no container name is specified "spec.containers[2]" (container with
378+
index 2 in this pod). This syntax is chosen only to have some well-defined way of
379+
referencing a part of an object.
380+
type: string
381+
kind:
382+
description: |-
383+
Kind of the referent.
384+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
385+
type: string
386+
name:
387+
description: |-
388+
Name of the referent.
389+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
390+
type: string
391+
namespace:
392+
description: |-
393+
Namespace of the referent.
394+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/
395+
type: string
396+
resourceVersion:
397+
description: |-
398+
Specific resourceVersion to which this reference is made, if any.
399+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
400+
type: string
401+
uid:
402+
description: |-
403+
UID of the referent.
404+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids
405+
type: string
406+
type: object
407+
x-kubernetes-map-type: atomic
357408
transportType:
358409
default: http
359410
description: |-
@@ -428,6 +479,52 @@ spec:
428479
- type
429480
type: object
430481
type: array
482+
toolGatewayRef:
483+
description: |-
484+
ToolGatewayRef references the ToolGateway resource that this tool server is connected to.
485+
This field is automatically populated by the controller when a Tool Gateway is being used.
486+
If nil, the tool server is not connected to any Tool Gateway.
487+
properties:
488+
apiVersion:
489+
description: API version of the referent.
490+
type: string
491+
fieldPath:
492+
description: |-
493+
If referring to a piece of an object instead of an entire object, this string
494+
should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2].
495+
For example, if the object reference is to a container within a pod, this would take on a value like:
496+
"spec.containers{name}" (where "name" refers to the name of the container that triggered
497+
the event) or if no container name is specified "spec.containers[2]" (container with
498+
index 2 in this pod). This syntax is chosen only to have some well-defined way of
499+
referencing a part of an object.
500+
type: string
501+
kind:
502+
description: |-
503+
Kind of the referent.
504+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
505+
type: string
506+
name:
507+
description: |-
508+
Name of the referent.
509+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
510+
type: string
511+
namespace:
512+
description: |-
513+
Namespace of the referent.
514+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/
515+
type: string
516+
resourceVersion:
517+
description: |-
518+
Specific resourceVersion to which this reference is made, if any.
519+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
520+
type: string
521+
uid:
522+
description: |-
523+
UID of the referent.
524+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids
525+
type: string
526+
type: object
527+
x-kubernetes-map-type: atomic
431528
url:
432529
description: |-
433530
Url is the cluster-local URL where this tool server can be accessed

config/rbac/role.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ rules:
6464
- runtime.agentic-layer.ai
6565
resources:
6666
- aigateways
67+
- toolgateways
6768
verbs:
6869
- get
6970
- list

internal/controller/toolserver_controller.go

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
ctrl "sigs.k8s.io/controller-runtime"
3434
"sigs.k8s.io/controller-runtime/pkg/client"
3535
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
36+
"sigs.k8s.io/controller-runtime/pkg/handler"
3637
logf "sigs.k8s.io/controller-runtime/pkg/log"
3738
)
3839

@@ -51,6 +52,7 @@ type ToolServerReconciler struct {
5152
// +kubebuilder:rbac:groups=runtime.agentic-layer.ai,resources=toolservers,verbs=get;list;watch;create;update;patch;delete
5253
// +kubebuilder:rbac:groups=runtime.agentic-layer.ai,resources=toolservers/status,verbs=get;update;patch
5354
// +kubebuilder:rbac:groups=runtime.agentic-layer.ai,resources=toolservers/finalizers,verbs=update
55+
// +kubebuilder:rbac:groups=runtime.agentic-layer.ai,resources=toolgateways,verbs=get;list;watch
5456
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
5557
// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
5658

@@ -75,6 +77,13 @@ func (r *ToolServerReconciler) Reconcile(ctx context.Context, req ctrl.Request)
7577

7678
log.Info("Reconciling ToolServer")
7779

80+
// Resolve ToolGateway (optional - returns nil if not found)
81+
toolGateway, err := r.resolveToolGateway(ctx, &toolServer)
82+
if err != nil {
83+
log.Error(err, "Failed to resolve ToolGateway")
84+
return ctrl.Result{}, err
85+
}
86+
7887
// Ensure Deployment exists and is up to date for http/sse transports
7988
if err := r.ensureDeployment(ctx, &toolServer); err != nil {
8089
log.Error(err, "Failed to ensure Deployment")
@@ -94,7 +103,7 @@ func (r *ToolServerReconciler) Reconcile(ctx context.Context, req ctrl.Request)
94103
}
95104

96105
// Update ToolServer status to Ready (optimistic)
97-
if err := r.updateToolServerStatusReady(ctx, &toolServer); err != nil {
106+
if err := r.updateToolServerStatusReady(ctx, &toolServer, toolGateway); err != nil {
98107
log.Error(err, "Failed to update ToolServer status")
99108
return ctrl.Result{}, err
100109
}
@@ -104,12 +113,25 @@ func (r *ToolServerReconciler) Reconcile(ctx context.Context, req ctrl.Request)
104113

105114
// SetupWithManager sets up the controller with the Manager.
106115
func (r *ToolServerReconciler) SetupWithManager(mgr ctrl.Manager) error {
107-
return ctrl.NewControllerManagedBy(mgr).
116+
log := logf.FromContext(context.Background())
117+
118+
builder := ctrl.NewControllerManagedBy(mgr).
108119
For(&runtimev1alpha1.ToolServer{}).
109120
Owns(&appsv1.Deployment{}).
110-
Owns(&corev1.Service{}).
111-
Named("toolserver").
112-
Complete(r)
121+
Owns(&corev1.Service{})
122+
123+
// Only watch ToolGateway if the CRD is installed
124+
if isToolGatewayCRDInstalled(mgr) {
125+
log.Info("ToolGateway CRD detected, enabling watch")
126+
builder = builder.Watches(
127+
&runtimev1alpha1.ToolGateway{},
128+
handler.EnqueueRequestsFromMapFunc(r.findToolServersReferencingToolGateway),
129+
)
130+
} else {
131+
log.Info("ToolGateway CRD not installed, skipping watch (tool server will work without Tool Gateway integration)")
132+
}
133+
134+
return builder.Named("toolserver").Complete(r)
113135
}
114136

115137
// ensureDeployment ensures the Deployment for the ToolServer exists and is up to date
@@ -283,8 +305,8 @@ func (r *ToolServerReconciler) buildReadinessProbe(port int32) *corev1.Probe {
283305
}
284306
}
285307

286-
// updateToolServerStatusReady sets the ToolServer status to Ready and updates the URL
287-
func (r *ToolServerReconciler) updateToolServerStatusReady(ctx context.Context, toolServer *runtimev1alpha1.ToolServer) error {
308+
// updateToolServerStatusReady sets the ToolServer status to Ready and updates the URL and ToolGatewayRef
309+
func (r *ToolServerReconciler) updateToolServerStatusReady(ctx context.Context, toolServer *runtimev1alpha1.ToolServer, toolGateway *runtimev1alpha1.ToolGateway) error {
288310
// Build URL for http/sse transports
289311
if toolServer.Spec.TransportType == httpTransport || toolServer.Spec.TransportType == sseTransport {
290312
toolServer.Status.Url = fmt.Sprintf("http://%s.%s.svc.cluster.local:%d%s",
@@ -293,6 +315,18 @@ func (r *ToolServerReconciler) updateToolServerStatusReady(ctx context.Context,
293315
toolServer.Status.Url = ""
294316
}
295317

318+
// Set ToolGatewayRef if a Tool Gateway is being used
319+
if toolGateway != nil {
320+
toolServer.Status.ToolGatewayRef = &corev1.ObjectReference{
321+
Kind: "ToolGateway",
322+
Namespace: toolGateway.Namespace,
323+
Name: toolGateway.Name,
324+
APIVersion: runtimev1alpha1.GroupVersion.String(),
325+
}
326+
} else {
327+
toolServer.Status.ToolGatewayRef = nil
328+
}
329+
296330
// Set Ready condition to True
297331
meta.SetStatusCondition(&toolServer.Status.Conditions, metav1.Condition{
298332
Type: "Ready",
@@ -311,6 +345,9 @@ func (r *ToolServerReconciler) updateToolServerStatusReady(ctx context.Context,
311345

312346
// updateToolServerStatusNotReady sets the ToolServer status to not Ready
313347
func (r *ToolServerReconciler) updateToolServerStatusNotReady(ctx context.Context, toolServer *runtimev1alpha1.ToolServer, reason, message string) error {
348+
// Clear the ToolGatewayRef since the tool server is not ready
349+
toolServer.Status.ToolGatewayRef = nil
350+
314351
// Set Ready condition to False
315352
meta.SetStatusCondition(&toolServer.Status.Conditions, metav1.Condition{
316353
Type: "Ready",
@@ -346,3 +383,10 @@ func getOrDefaultToolServerResourceRequirements(toolServer *runtimev1alpha1.Tool
346383
},
347384
}
348385
}
386+
387+
// isToolGatewayCRDInstalled checks if the ToolGateway CRD is installed in the cluster
388+
func isToolGatewayCRDInstalled(mgr ctrl.Manager) bool {
389+
gvk := runtimev1alpha1.GroupVersion.WithKind("ToolGateway")
390+
_, err := mgr.GetRESTMapper().RESTMapping(gvk.GroupKind(), gvk.Version)
391+
return err == nil
392+
}

0 commit comments

Comments
 (0)