Skip to content

Commit 77ce5f9

Browse files
Merge pull request #242 from varshaprasad96/improve-agentruntime-controller
feat(AgentRuntime): add validating webhook for duplicate targetRef rejection
2 parents 06e0419 + 3de1df7 commit 77ce5f9

6 files changed

Lines changed: 435 additions & 4 deletions

File tree

kagenti-operator/cmd/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,10 @@ func main() {
338338
setupLog.Error(err, "unable to create webhook", "webhook", "AgentCard")
339339
os.Exit(1)
340340
}
341+
if err = webhookv1alpha1.SetupAgentRuntimeWebhookWithManager(mgr); err != nil {
342+
setupLog.Error(err, "unable to create webhook", "webhook", "AgentRuntime")
343+
os.Exit(1)
344+
}
341345
// +kubebuilder:scaffold:builder
342346

343347
if metricsCertWatcher != nil {

kagenti-operator/config/webhook/manifests.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,23 @@ webhooks:
2424
resources:
2525
- agentcards
2626
sideEffects: None
27+
- admissionReviewVersions:
28+
- v1
29+
clientConfig:
30+
service:
31+
name: webhook-service
32+
namespace: system
33+
path: /validate-agent-kagenti-dev-v1alpha1-agentruntime
34+
failurePolicy: Fail
35+
name: vagentruntime.kb.io
36+
rules:
37+
- apiGroups:
38+
- agent.kagenti.dev
39+
apiVersions:
40+
- v1alpha1
41+
operations:
42+
- CREATE
43+
- UPDATE
44+
resources:
45+
- agentruntimes
46+
sideEffects: None

kagenti-operator/docs/api-reference.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,15 @@ Configures observability for an AgentRuntime.
465465
| `Ready` | False | `ConfigHashError` | Failed to compute the config hash |
466466
| `Ready` | False | `ConfigApplyError` | Failed to apply labels/annotations to the workload |
467467

468+
### Admission Validation
469+
470+
A validating webhook prevents ownership conflicts:
471+
472+
- **Duplicate targetRef rejection**: If an AgentRuntime CR already targets a given workload (same `apiVersion` + `kind` + `name`) in the same namespace, creating or updating another AgentRuntime to target the same workload is rejected at admission time.
473+
- **Fail-open on API errors**: If the webhook's internal list call fails (e.g., transient API server error), the request is allowed through to avoid blocking deployments. Note: the Kubernetes-level `failurePolicy` is set to `Fail`, so if the webhook pod itself is unreachable, the API server will reject AgentRuntime creates/updates. This is consistent with the AgentCard webhook.
474+
475+
This prevents conflicting label updates where two AgentRuntime CRs with different `type` values (e.g., `agent` vs `tool`) would fight over the same workload's `kagenti.io/type` label.
476+
468477
### Examples
469478

470479
#### Basic Agent Runtime

kagenti-operator/docs/architecture.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,9 @@ The Kagenti Operator is a Kubernetes controller that implements the [Operator Pa
7676

7777
### Supporting Components
7878

79-
#### Webhook
80-
- Validates AgentCard resources
81-
- Ensures `targetRef` is set on AgentCards
82-
- Mutates resources with default values
79+
#### Webhooks
80+
- **AgentCard Validator**: Ensures `targetRef` is set on AgentCards. Rejects duplicate `targetRef` entries (prevents multiple AgentCards targeting the same workload in a namespace).
81+
- **AgentRuntime Validator**: Rejects duplicate `targetRef` entries (prevents multiple AgentRuntime CRs targeting the same workload in a namespace). Uses authoritative API server reads to eliminate informer cache-lag races.
8382

8483
#### Signature Providers
8584
- **X5CProvider**: Validates `x5c` certificate chains against the SPIRE X.509 trust bundle and verifies JWS signatures using the leaf public key
@@ -103,6 +102,7 @@ graph TB
103102
SyncController[AgentCardSync Controller]
104103
RuntimeController[AgentRuntime Controller]
105104
CardCR -->|Validates| Webhook
105+
RuntimeCR -->|Validates| Webhook
106106
107107
Webhook -->|Valid CR| CardController
108108
end
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
agentv1alpha1 "github.com/kagenti/operator/api/v1alpha1"
24+
"k8s.io/apimachinery/pkg/runtime"
25+
ctrl "sigs.k8s.io/controller-runtime"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
28+
)
29+
30+
var agentruntimelog = ctrl.Log.WithName("agentruntime-webhook")
31+
32+
func SetupAgentRuntimeWebhookWithManager(mgr ctrl.Manager) error {
33+
return ctrl.NewWebhookManagedBy(mgr).
34+
For(&agentv1alpha1.AgentRuntime{}).
35+
WithValidator(&AgentRuntimeValidator{Reader: mgr.GetAPIReader()}).
36+
Complete()
37+
}
38+
39+
// +kubebuilder:webhook:path=/validate-agent-kagenti-dev-v1alpha1-agentruntime,mutating=false,failurePolicy=fail,sideEffects=None,groups=agent.kagenti.dev,resources=agentruntimes,verbs=create;update,versions=v1alpha1,name=vagentruntime.kb.io,admissionReviewVersions=v1
40+
41+
type AgentRuntimeValidator struct {
42+
// Reader is an uncached client for authoritative reads from the API server.
43+
// Used for duplicate targetRef checks during admission. Nil-safe: the check
44+
// is skipped when Reader is nil (e.g., in unit tests without a real API server).
45+
Reader client.Reader
46+
}
47+
48+
func (v *AgentRuntimeValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
49+
rt, ok := obj.(*agentv1alpha1.AgentRuntime)
50+
if !ok {
51+
return nil, fmt.Errorf("expected an AgentRuntime but got a %T", obj)
52+
}
53+
54+
agentruntimelog.Info("validate create", "name", rt.Name)
55+
56+
if err := v.checkDuplicateTargetRef(ctx, rt); err != nil {
57+
return nil, err
58+
}
59+
60+
return nil, nil
61+
}
62+
63+
func (v *AgentRuntimeValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
64+
rt, ok := newObj.(*agentv1alpha1.AgentRuntime)
65+
if !ok {
66+
return nil, fmt.Errorf("expected an AgentRuntime but got a %T", newObj)
67+
}
68+
69+
agentruntimelog.Info("validate update", "name", rt.Name)
70+
71+
if err := v.checkDuplicateTargetRef(ctx, rt); err != nil {
72+
return nil, err
73+
}
74+
75+
return nil, nil
76+
}
77+
78+
func (v *AgentRuntimeValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
79+
rt, ok := obj.(*agentv1alpha1.AgentRuntime)
80+
if !ok {
81+
return nil, fmt.Errorf("expected an AgentRuntime but got a %T", obj)
82+
}
83+
84+
agentruntimelog.Info("validate delete", "name", rt.Name)
85+
86+
return nil, nil
87+
}
88+
89+
// checkDuplicateTargetRef rejects creation/update if another AgentRuntime already
90+
// targets the same workload (apiVersion + kind + name) in the same namespace.
91+
func (v *AgentRuntimeValidator) checkDuplicateTargetRef(ctx context.Context, rt *agentv1alpha1.AgentRuntime) error {
92+
if v.Reader == nil {
93+
return nil
94+
}
95+
96+
ref := rt.Spec.TargetRef
97+
98+
rtList := &agentv1alpha1.AgentRuntimeList{}
99+
// fail-open: allow creation if we can't verify uniqueness
100+
if err := v.Reader.List(ctx, rtList, client.InNamespace(rt.Namespace)); err != nil {
101+
agentruntimelog.Error(err, "failed to list AgentRuntimes for duplicate check")
102+
return nil
103+
}
104+
105+
for i := range rtList.Items {
106+
existing := &rtList.Items[i]
107+
if existing.Name == rt.Name {
108+
continue
109+
}
110+
if existing.Spec.TargetRef.APIVersion == ref.APIVersion &&
111+
existing.Spec.TargetRef.Kind == ref.Kind &&
112+
existing.Spec.TargetRef.Name == ref.Name {
113+
return fmt.Errorf(
114+
"an AgentRuntime already targets %s %s in namespace %s: %s",
115+
ref.Kind, ref.Name, rt.Namespace, existing.Name,
116+
)
117+
}
118+
}
119+
120+
return nil
121+
}

0 commit comments

Comments
 (0)