Skip to content

Commit 273d943

Browse files
committed
feat: Add default framework parameter to configuration
1 parent 092184e commit 273d943

7 files changed

Lines changed: 178 additions & 43 deletions

File tree

api/v1alpha1/agentruntimeconfiguration_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ import (
2222

2323
// AgentRuntimeConfigurationSpec defines the desired state of AgentRuntimeConfiguration.
2424
type AgentRuntimeConfigurationSpec struct {
25+
// DefaultFramework defines the default agent framework to use when an Agent resource
26+
// does not specify a framework. When this value changes, all agents without an explicit
27+
// framework will be updated to use the new default.
28+
// If not specified, the operator defaults to "google-adk".
29+
// +kubebuilder:validation:Enum=google-adk;msaf
30+
// +optional
31+
DefaultFramework string `json:"defaultFramework,omitempty"`
32+
2533
// AgentTemplateImages defines the default template images for different agent frameworks.
2634
// These images are used when an Agent resource does not specify a custom image.
2735
// +optional
@@ -50,6 +58,7 @@ type AgentRuntimeConfigurationStatus struct {
5058

5159
// +kubebuilder:object:root=true
5260
// +kubebuilder:subresource:status
61+
// +kubebuilder:printcolumn:name="Default Framework",type=string,JSONPath=`.spec.defaultFramework`
5362
// +kubebuilder:printcolumn:name="Google ADK Image",type=string,JSONPath=`.spec.agentTemplateImages.googleAdk`
5463
// +kubebuilder:printcolumn:name="MSAF Image",type=string,JSONPath=`.spec.agentTemplateImages.msaf`
5564
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ spec:
1515
scope: Namespaced
1616
versions:
1717
- additionalPrinterColumns:
18+
- jsonPath: .spec.defaultFramework
19+
name: Default Framework
20+
type: string
1821
- jsonPath: .spec.agentTemplateImages.googleAdk
1922
name: Google ADK Image
2023
type: string
@@ -68,6 +71,16 @@ spec:
6871
If not specified, the operator's built-in default will be used.
6972
type: string
7073
type: object
74+
defaultFramework:
75+
description: |-
76+
DefaultFramework defines the default agent framework to use when an Agent resource
77+
does not specify a framework. When this value changes, all agents without an explicit
78+
framework will be updated to use the new default.
79+
If not specified, the operator defaults to "google-adk".
80+
enum:
81+
- google-adk
82+
- msaf
83+
type: string
7184
type: object
7285
status:
7386
description: AgentRuntimeConfigurationStatus defines the observed state

config/samples/runtime_v1alpha1_agentruntimeconfiguration.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ metadata:
44
name: default
55
namespace: agent-runtime-operator-system
66
spec:
7+
# Default framework used for agents that don't specify one explicitly.
8+
# When changed, all agents without an explicit framework will be updated.
9+
# Supported values: google-adk, msaf, custom
10+
defaultFramework: "google-adk"
711
agentTemplateImages:
812
# Customize the default template image for Google ADK framework agents
913
# This will be used when an Agent resource does not specify a custom image

internal/controller/agent_reconciler.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,14 @@ func (r *AgentReconciler) ensureDeployment(ctx context.Context, agent *runtimev1
166166
},
167167
}
168168

169+
// Resolve the effective framework (from agent spec or config default)
170+
effectiveFramework := resolveEffectiveFramework(agent, runtimeConfig)
171+
169172
if op, err := controllerutil.CreateOrUpdate(ctx, r.Client, deployment, func() error {
170173
// Build managed labels
171174
managedLabels := map[string]string{
172175
"app": agent.Name,
173-
"framework": agent.Spec.Framework,
176+
"framework": effectiveFramework,
174177
}
175178

176179
// Selector labels (immutable)
@@ -248,7 +251,7 @@ func (r *AgentReconciler) ensureDeployment(ctx context.Context, agent *runtimev1
248251
container.Image = agent.Spec.Image
249252
} else {
250253
// Use image from runtime configuration if available, otherwise use built-in defaults
251-
container.Image = r.getTemplateImage(agent.Spec.Framework, runtimeConfig)
254+
container.Image = r.getTemplateImage(effectiveFramework, runtimeConfig)
252255
}
253256
container.Ports = containerPorts
254257
container.Env = allEnvVars
@@ -303,8 +306,7 @@ func (r *AgentReconciler) ensureService(ctx context.Context, agent *runtimev1alp
303306
if op, err := controllerutil.CreateOrUpdate(ctx, r.Client, service, func() error {
304307
// Build managed labels
305308
managedLabels := map[string]string{
306-
"app": agent.Name,
307-
"framework": agent.Spec.Framework,
309+
"app": agent.Name,
308310
}
309311

310312
// Service selector (stable labels only)
@@ -482,6 +484,20 @@ func isNoMatchError(err error) bool {
482484
return meta.IsNoMatchError(err)
483485
}
484486

487+
// resolveEffectiveFramework returns the effective framework for an agent.
488+
// If the agent specifies a framework explicitly, that is used.
489+
// Otherwise, the default framework from AgentRuntimeConfiguration is used.
490+
// If no configuration exists, falls back to the built-in default (google-adk).
491+
func resolveEffectiveFramework(agent *runtimev1alpha1.Agent, config *runtimev1alpha1.AgentRuntimeConfiguration) string {
492+
if agent.Spec.Framework != "" {
493+
return agent.Spec.Framework
494+
}
495+
if config != nil && config.Spec.DefaultFramework != "" {
496+
return config.Spec.DefaultFramework
497+
}
498+
return googleAdkFramework
499+
}
500+
485501
// getTemplateImage returns the appropriate template image for the given framework.
486502
// Resolution priority: AgentRuntimeConfiguration → built-in defaults
487503
func (r *AgentReconciler) getTemplateImage(framework string, config *runtimev1alpha1.AgentRuntimeConfiguration) string {

internal/controller/agent_reconciler_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,113 @@ var _ = Describe("Agent Controller", func() {
650650
Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(Equal(agentImage))
651651
})
652652

653+
It("should use default framework from AgentRuntimeConfiguration when agent has no framework", func() {
654+
config := &runtimev1alpha1.AgentRuntimeConfiguration{
655+
ObjectMeta: metav1.ObjectMeta{
656+
Name: "test-config-default-fw",
657+
Namespace: "default",
658+
},
659+
Spec: runtimev1alpha1.AgentRuntimeConfigurationSpec{
660+
DefaultFramework: "msaf",
661+
},
662+
}
663+
Expect(k8sClient.Create(ctx, config)).To(Succeed())
664+
665+
agent := &runtimev1alpha1.Agent{
666+
ObjectMeta: metav1.ObjectMeta{
667+
Name: "test-agent-default-fw",
668+
Namespace: "default",
669+
},
670+
Spec: runtimev1alpha1.AgentSpec{
671+
// No framework specified - should use config's defaultFramework
672+
Protocols: []runtimev1alpha1.AgentProtocol{
673+
{Type: runtimev1alpha1.A2AProtocol, Port: 8000, Path: "/", Name: "a2a"},
674+
},
675+
},
676+
}
677+
Expect(k8sClient.Create(ctx, agent)).To(Succeed())
678+
679+
_, err := reconciler.Reconcile(ctx, reconcile.Request{
680+
NamespacedName: types.NamespacedName{Name: "test-agent-default-fw", Namespace: "default"},
681+
})
682+
Expect(err).NotTo(HaveOccurred())
683+
684+
deployment := &appsv1.Deployment{}
685+
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-agent-default-fw", Namespace: "default"}, deployment)).To(Succeed())
686+
Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1))
687+
// Should use MSAF template image since defaultFramework is msaf
688+
Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(Equal(defaultTemplateImageMsaf))
689+
// Labels should reflect the effective framework
690+
Expect(deployment.Labels["framework"]).To(Equal("msaf"))
691+
})
692+
693+
It("should fall back to google-adk when agent has no framework and config has no defaultFramework", func() {
694+
agent := &runtimev1alpha1.Agent{
695+
ObjectMeta: metav1.ObjectMeta{
696+
Name: "test-agent-fallback-fw",
697+
Namespace: "default",
698+
},
699+
Spec: runtimev1alpha1.AgentSpec{
700+
// No framework specified, no config exists
701+
Protocols: []runtimev1alpha1.AgentProtocol{
702+
{Type: runtimev1alpha1.A2AProtocol, Port: 8000, Path: "/", Name: "a2a"},
703+
},
704+
},
705+
}
706+
Expect(k8sClient.Create(ctx, agent)).To(Succeed())
707+
708+
_, err := reconciler.Reconcile(ctx, reconcile.Request{
709+
NamespacedName: types.NamespacedName{Name: "test-agent-fallback-fw", Namespace: "default"},
710+
})
711+
Expect(err).NotTo(HaveOccurred())
712+
713+
deployment := &appsv1.Deployment{}
714+
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-agent-fallback-fw", Namespace: "default"}, deployment)).To(Succeed())
715+
Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1))
716+
// Should fall back to google-adk built-in default
717+
Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(Equal(defaultTemplateImageAdk))
718+
Expect(deployment.Labels["framework"]).To(Equal("google-adk"))
719+
})
720+
721+
It("should use agent's explicit framework even when config has a different default", func() {
722+
config := &runtimev1alpha1.AgentRuntimeConfiguration{
723+
ObjectMeta: metav1.ObjectMeta{
724+
Name: "test-config-explicit-fw",
725+
Namespace: "default",
726+
},
727+
Spec: runtimev1alpha1.AgentRuntimeConfigurationSpec{
728+
DefaultFramework: "msaf",
729+
},
730+
}
731+
Expect(k8sClient.Create(ctx, config)).To(Succeed())
732+
733+
agent := &runtimev1alpha1.Agent{
734+
ObjectMeta: metav1.ObjectMeta{
735+
Name: "test-agent-explicit-fw",
736+
Namespace: "default",
737+
},
738+
Spec: runtimev1alpha1.AgentSpec{
739+
Framework: "google-adk", // Explicit framework should override config default
740+
Protocols: []runtimev1alpha1.AgentProtocol{
741+
{Type: runtimev1alpha1.A2AProtocol, Port: 8000, Path: "/", Name: "a2a"},
742+
},
743+
},
744+
}
745+
Expect(k8sClient.Create(ctx, agent)).To(Succeed())
746+
747+
_, err := reconciler.Reconcile(ctx, reconcile.Request{
748+
NamespacedName: types.NamespacedName{Name: "test-agent-explicit-fw", Namespace: "default"},
749+
})
750+
Expect(err).NotTo(HaveOccurred())
751+
752+
deployment := &appsv1.Deployment{}
753+
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-agent-explicit-fw", Namespace: "default"}, deployment)).To(Succeed())
754+
Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1))
755+
// Should use google-adk image since agent explicitly specifies it
756+
Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(Equal(defaultTemplateImageAdk))
757+
Expect(deployment.Labels["framework"]).To(Equal("google-adk"))
758+
})
759+
653760
It("should fail reconciliation when multiple AgentRuntimeConfigurations exist", func() {
654761
config1 := &runtimev1alpha1.AgentRuntimeConfiguration{
655762
ObjectMeta: metav1.ObjectMeta{

internal/webhook/v1alpha1/agent_webhook.go

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,9 @@ type AgentWebhookConfig struct {
5151
func SetupAgentWebhookWithManager(mgr ctrl.Manager) error {
5252
return ctrl.NewWebhookManagedBy(mgr, &runtimev1alpha1.Agent{}).
5353
WithDefaulter(&AgentCustomDefaulter{
54-
DefaultFramework: googleAdkFramework,
55-
DefaultReplicas: 1,
56-
DefaultPort: 8080,
57-
DefaultPortGoogleAdk: 8000,
58-
DefaultPortMsaf: 8000,
59-
Recorder: mgr.GetEventRecorder("agent-defaulter-webhook"),
54+
DefaultReplicas: 1,
55+
DefaultPort: 8000,
56+
Recorder: mgr.GetEventRecorder("agent-defaulter-webhook"),
6057
}).
6158
WithValidator(&AgentCustomValidator{}).
6259
Complete()
@@ -70,12 +67,9 @@ func SetupAgentWebhookWithManager(mgr ctrl.Manager) error {
7067
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
7168
// as it is used only for temporary operations and does not need to be deeply copied.
7269
type AgentCustomDefaulter struct {
73-
DefaultFramework string
74-
DefaultReplicas int32
75-
DefaultPort int32
76-
DefaultPortGoogleAdk int32
77-
DefaultPortMsaf int32
78-
Recorder events.EventRecorder
70+
DefaultReplicas int32
71+
DefaultPort int32
72+
Recorder events.EventRecorder
7973
}
8074

8175
// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Agent.
@@ -89,10 +83,9 @@ func (d *AgentCustomDefaulter) Default(_ context.Context, agent *runtimev1alpha1
8983

9084
// applyDefaults applies default values to the Agent.
9185
func (d *AgentCustomDefaulter) applyDefaults(agent *runtimev1alpha1.Agent) {
92-
// Set default framework if not specified
93-
if agent.Spec.Framework == "" {
94-
agent.Spec.Framework = d.DefaultFramework
95-
}
86+
// Note: Framework is intentionally NOT defaulted here. When left empty, the controller
87+
// resolves the effective framework from AgentRuntimeConfiguration.spec.defaultFramework.
88+
// This allows changing the default framework in the config to propagate to all agents.
9689

9790
// Set default replicas if not specified
9891
if agent.Spec.Replicas == nil {
@@ -110,25 +103,14 @@ func (d *AgentCustomDefaulter) applyDefaults(agent *runtimev1alpha1.Agent) {
110103
// Set default ports for protocols if not specified
111104
for i, protocol := range agent.Spec.Protocols {
112105
if protocol.Port == 0 {
113-
agent.Spec.Protocols[i].Port = d.frameworkDefaultPort(agent.Spec.Framework)
106+
agent.Spec.Protocols[i].Port = d.DefaultPort
114107
}
115108
if protocol.Name == "" {
116109
agent.Spec.Protocols[i].Name = fmt.Sprintf("%s-%d", sanitizeForPortName(protocol.Type), agent.Spec.Protocols[i].Port)
117110
}
118111
}
119112
}
120113

121-
func (d *AgentCustomDefaulter) frameworkDefaultPort(framework string) int32 {
122-
switch framework {
123-
case googleAdkFramework:
124-
return d.DefaultPortGoogleAdk
125-
case msafFramework:
126-
return d.DefaultPortMsaf
127-
default:
128-
return d.DefaultPort // Default port for unknown frameworks
129-
}
130-
}
131-
132114
// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here.
133115
// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook.
134116
// +kubebuilder:webhook:path=/validate-runtime-agentic-layer-ai-v1alpha1-agent,mutating=false,failurePolicy=fail,sideEffects=None,groups=runtime.agentic-layer.ai,resources=agents,verbs=create;update,versions=v1alpha1,name=vagent-v1alpha1.kb.io,admissionReviewVersions=v1
@@ -164,11 +146,12 @@ func (v *AgentCustomValidator) validateAgent(agent *runtimev1alpha1.Agent) (admi
164146
var allErrs field.ErrorList
165147

166148
// Validate framework and image combination
149+
// Empty framework is allowed - the controller resolves it from AgentRuntimeConfiguration.
167150
templateFrameworks := map[string]bool{
168151
googleAdkFramework: true,
169152
msafFramework: true,
170153
}
171-
if agent.Spec.Image == "" && !templateFrameworks[agent.Spec.Framework] {
154+
if agent.Spec.Framework != "" && agent.Spec.Image == "" && !templateFrameworks[agent.Spec.Framework] {
172155
allErrs = append(allErrs, field.Invalid(
173156
field.NewPath("spec", "framework"),
174157
agent.Spec.Framework,

internal/webhook/v1alpha1/agent_webhook_test.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,9 @@ var _ = Describe("Agent Webhook", func() {
5454
ctx = context.Background()
5555
recorder = events.NewFakeRecorder(10)
5656
defaulter = AgentCustomDefaulter{
57-
DefaultReplicas: 1,
58-
DefaultPort: 8080,
59-
DefaultPortGoogleAdk: 8000,
60-
Recorder: recorder,
57+
DefaultReplicas: 1,
58+
DefaultPort: 8000,
59+
Recorder: recorder,
6160
}
6261
validator = AgentCustomValidator{}
6362
obj = &runtimev1alpha1.Agent{
@@ -124,7 +123,7 @@ var _ = Describe("Agent Webhook", func() {
124123

125124
By("verifying that default port is set")
126125
Expect(obj.Spec.Protocols).To(HaveLen(1))
127-
Expect(obj.Spec.Protocols[0].Port).To(Equal(int32(8080)))
126+
Expect(obj.Spec.Protocols[0].Port).To(Equal(int32(8000)))
128127
})
129128

130129
It("Should not override existing port", func() {
@@ -145,21 +144,21 @@ var _ = Describe("Agent Webhook", func() {
145144
It("Should set default protocol name when not specified", func() {
146145
By("setting up a protocol without name")
147146
obj.Spec.Protocols = []runtimev1alpha1.AgentProtocol{
148-
{Type: runtimev1alpha1.A2AProtocol, Port: 8080},
147+
{Type: runtimev1alpha1.A2AProtocol, Port: 8000},
149148
}
150149

151150
By("calling the Default method")
152151
err := defaulter.Default(ctx, obj)
153152
Expect(err).NotTo(HaveOccurred())
154153

155154
By("verifying that protocol name is generated with lowercase")
156-
Expect(obj.Spec.Protocols[0].Name).To(Equal("a2a-8080"))
155+
Expect(obj.Spec.Protocols[0].Name).To(Equal("a2a-8000"))
157156
})
158157

159158
It("Should not override existing protocol name", func() {
160159
By("setting up a protocol with custom name")
161160
obj.Spec.Protocols = []runtimev1alpha1.AgentProtocol{
162-
{Type: runtimev1alpha1.A2AProtocol, Port: 8080, Name: "custom-name"},
161+
{Type: runtimev1alpha1.A2AProtocol, Port: 8000, Name: "custom-name"},
163162
}
164163

165164
By("calling the Default method")
@@ -227,15 +226,19 @@ var _ = Describe("Agent Webhook", func() {
227226
err := defaulter.Default(ctx, emptyAgent)
228227
Expect(err).NotTo(HaveOccurred())
229228

229+
By("verifying that framework is NOT defaulted (resolved at controller time)")
230+
Expect(emptyAgent.Spec.Framework).To(BeEmpty())
231+
230232
By("verifying that default replicas are set")
231233
Expect(emptyAgent.Spec.Replicas).NotTo(BeNil())
232234
Expect(*emptyAgent.Spec.Replicas).To(Equal(int32(1)))
233235

234236
By("verifying that protocol list is populated with default protocol")
235237
Expect(emptyAgent.Spec.Protocols).To(HaveLen(1))
236238
Expect(emptyAgent.Spec.Protocols[0].Type).To(Equal(runtimev1alpha1.A2AProtocol))
237-
Expect(emptyAgent.Spec.Protocols[0].Name).To(Equal("a2a-8080"))
238-
Expect(emptyAgent.Spec.Protocols[0].Port).To(Equal(int32(8080)))
239+
// Port is resolved using the fallback default framework (google-adk) for port defaulting
240+
Expect(emptyAgent.Spec.Protocols[0].Name).To(Equal("a2a-8000"))
241+
Expect(emptyAgent.Spec.Protocols[0].Port).To(Equal(int32(8000)))
239242
})
240243

241244
It("Should handle multiple protocols with mixed configurations", func() {

0 commit comments

Comments
 (0)