Skip to content

Commit 3c27e3b

Browse files
nimishamehta5EItanyapeterj
authored
Read tool server configuration from a secret or configmap (#416)
* Read tool server configuration from a secret or configmap Fixes: #374 Signed-off-by: Nimisha Mehta <nimishamehta5@gmail.com> address comments Signed-off-by: Nimisha Mehta <nimishamehta5@gmail.com> address API comments Signed-off-by: Nimisha Mehta <nimishamehta5@gmail.com> * address comments Signed-off-by: Nimisha Mehta <nimishamehta5@gmail.com> --------- Signed-off-by: Nimisha Mehta <nimishamehta5@gmail.com> Co-authored-by: Eitan Yarmush <eitan.yarmush@solo.io> Co-authored-by: Peter Jausovec <peterj@users.noreply.github.com>
1 parent 6e515fd commit 3c27e3b

File tree

6 files changed

+497
-3
lines changed

6 files changed

+497
-3
lines changed

go/config/crd/bases/kagent.dev_toolservers.yaml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,43 @@ spec:
4747
properties:
4848
headers:
4949
x-kubernetes-preserve-unknown-fields: true
50+
headersFrom:
51+
items:
52+
description: ValueRef represents a configuration value
53+
properties:
54+
name:
55+
type: string
56+
value:
57+
type: string
58+
valueFrom:
59+
description: ValueSource defines a source for configuration
60+
values from a Secret or ConfigMap
61+
properties:
62+
key:
63+
type: string
64+
type:
65+
enum:
66+
- ConfigMap
67+
- Secret
68+
type: string
69+
valueRef:
70+
description: |-
71+
The reference to the ConfigMap or Secret. Can either be a reference to a resource in the same namespace,
72+
or a reference to a resource in a different namespace in the form "namespace/name".
73+
If namespace is not provided, the default namespace is used.
74+
type: string
75+
required:
76+
- key
77+
- type
78+
type: object
79+
required:
80+
- name
81+
type: object
82+
x-kubernetes-validations:
83+
- message: Exactly one of value or valueFrom must be specified
84+
rule: (has(self.value) && !has(self.valueFrom)) || (!has(self.value)
85+
&& has(self.valueFrom))
86+
type: array
5087
sse_read_timeout:
5188
type: string
5289
timeout:
@@ -68,6 +105,43 @@ spec:
68105
additionalProperties:
69106
type: string
70107
type: object
108+
envFrom:
109+
items:
110+
description: ValueRef represents a configuration value
111+
properties:
112+
name:
113+
type: string
114+
value:
115+
type: string
116+
valueFrom:
117+
description: ValueSource defines a source for configuration
118+
values from a Secret or ConfigMap
119+
properties:
120+
key:
121+
type: string
122+
type:
123+
enum:
124+
- ConfigMap
125+
- Secret
126+
type: string
127+
valueRef:
128+
description: |-
129+
The reference to the ConfigMap or Secret. Can either be a reference to a resource in the same namespace,
130+
or a reference to a resource in a different namespace in the form "namespace/name".
131+
If namespace is not provided, the default namespace is used.
132+
type: string
133+
required:
134+
- key
135+
- type
136+
type: object
137+
required:
138+
- name
139+
type: object
140+
x-kubernetes-validations:
141+
- message: Exactly one of value or valueFrom must be specified
142+
rule: (has(self.value) && !has(self.valueFrom)) || (!has(self.value)
143+
&& has(self.valueFrom))
144+
type: array
71145
required:
72146
- command
73147
type: object

go/controller/api/v1alpha1/toolserver_types.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,48 @@ type ToolServerConfig struct {
3131
Sse *SseMcpServerConfig `json:"sse,omitempty"`
3232
}
3333

34+
type ValueSourceType string
35+
36+
const (
37+
ConfigMapValueSource ValueSourceType = "ConfigMap"
38+
SecretValueSource ValueSourceType = "Secret"
39+
)
40+
41+
// ValueSource defines a source for configuration values from a Secret or ConfigMap
42+
type ValueSource struct {
43+
// +kubebuilder:validation:Enum=ConfigMap;Secret
44+
Type ValueSourceType `json:"type"`
45+
// The reference to the ConfigMap or Secret. Can either be a reference to a resource in the same namespace,
46+
// or a reference to a resource in a different namespace in the form "namespace/name".
47+
// If namespace is not provided, the default namespace is used.
48+
// +optional
49+
ValueRef string `json:"valueRef"`
50+
Key string `json:"key"`
51+
}
52+
53+
// ValueRef represents a configuration value
54+
// +kubebuilder:validation:XValidation:rule="(has(self.value) && !has(self.valueFrom)) || (!has(self.value) && has(self.valueFrom))",message="Exactly one of value or valueFrom must be specified"
55+
type ValueRef struct {
56+
Name string `json:"name"`
57+
// +optional
58+
Value string `json:"value,omitempty"`
59+
// +optional
60+
ValueFrom *ValueSource `json:"valueFrom,omitempty"`
61+
}
62+
3463
type StdioMcpServerConfig struct {
3564
Command string `json:"command"`
3665
Args []string `json:"args,omitempty"`
3766
Env map[string]string `json:"env,omitempty"`
67+
EnvFrom []ValueRef `json:"envFrom,omitempty"`
3868
}
3969

4070
type SseMcpServerConfig struct {
4171
URL string `json:"url"`
4272
// +kubebuilder:pruning:PreserveUnknownFields
4373
// +kubebuilder:validation:Schemaless
4474
Headers map[string]AnyType `json:"headers,omitempty"`
75+
HeadersFrom []ValueRef `json:"headersFrom,omitempty"`
4576
Timeout string `json:"timeout,omitempty"`
4677
SseReadTimeout string `json:"sse_read_timeout,omitempty"`
4778
}

go/controller/api/v1alpha1/zz_generated.deepcopy.go

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

go/controller/internal/autogen/autogen_api_translator.go

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
autogen_client "github.com/kagent-dev/kagent/go/autogen/client"
1414
"github.com/kagent-dev/kagent/go/controller/api/v1alpha1"
1515
common "github.com/kagent-dev/kagent/go/controller/internal/utils"
16+
corev1 "k8s.io/api/core/v1"
1617
v1 "k8s.io/api/core/v1"
1718
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1819
"k8s.io/apimachinery/pkg/types"
@@ -52,7 +53,7 @@ type apiTranslator struct {
5253

5354
func (a *apiTranslator) TranslateToolServer(ctx context.Context, toolServer *v1alpha1.ToolServer) (*autogen_client.ToolServer, error) {
5455
// provder = "kagent.tool_servers.StdioMcpToolServer" || "kagent.tool_servers.SseMcpToolServer"
55-
provider, toolServerConfig, err := translateToolServerConfig(toolServer.Spec.Config)
56+
provider, toolServerConfig, err := a.translateToolServerConfig(ctx, toolServer.Spec.Config, toolServer.Namespace)
5657
if err != nil {
5758
return nil, err
5859
}
@@ -70,21 +71,128 @@ func (a *apiTranslator) TranslateToolServer(ctx context.Context, toolServer *v1a
7071
}, nil
7172
}
7273

73-
func translateToolServerConfig(config v1alpha1.ToolServerConfig) (string, *api.ToolServerConfig, error) {
74+
// resolveValueSource resolves a value from a ValueSource
75+
func (a *apiTranslator) resolveValueSource(ctx context.Context, source *v1alpha1.ValueSource, namespace string) (string, error) {
76+
if source == nil {
77+
return "", fmt.Errorf("source cannot be nil")
78+
}
79+
80+
switch source.Type {
81+
case v1alpha1.ConfigMapValueSource:
82+
return a.getConfigMapValue(ctx, source, namespace)
83+
case v1alpha1.SecretValueSource:
84+
return a.getSecretValue(ctx, source, namespace)
85+
default:
86+
return "", fmt.Errorf("unknown value source type: %s", source.Type)
87+
}
88+
}
89+
90+
// getConfigMapValue fetches a value from a ConfigMap
91+
func (a *apiTranslator) getConfigMapValue(ctx context.Context, source *v1alpha1.ValueSource, namespace string) (string, error) {
92+
if source == nil {
93+
return "", fmt.Errorf("source cannot be nil")
94+
}
95+
96+
configMap := &corev1.ConfigMap{}
97+
err := fetchObjKube(
98+
ctx,
99+
a.kube,
100+
configMap,
101+
source.ValueRef,
102+
namespace,
103+
)
104+
if err != nil {
105+
return "", fmt.Errorf("failed to find ConfigMap for %s: %v", source.ValueRef, err)
106+
}
107+
108+
value, exists := configMap.Data[source.Key]
109+
if !exists {
110+
return "", fmt.Errorf("key %s not found in ConfigMap %s/%s", source.Key, configMap.Namespace, configMap.Name)
111+
}
112+
return value, nil
113+
}
114+
115+
// getSecretValue fetches a value from a Secret
116+
func (a *apiTranslator) getSecretValue(ctx context.Context, source *v1alpha1.ValueSource, namespace string) (string, error) {
117+
if source == nil {
118+
return "", fmt.Errorf("source cannot be nil")
119+
}
120+
121+
secret := &corev1.Secret{}
122+
err := fetchObjKube(
123+
ctx,
124+
a.kube,
125+
secret,
126+
source.ValueRef,
127+
namespace,
128+
)
129+
if err != nil {
130+
return "", fmt.Errorf("failed to find Secret for %s: %v", source.ValueRef, err)
131+
}
132+
133+
value, exists := secret.Data[source.Key]
134+
if !exists {
135+
return "", fmt.Errorf("key %s not found in Secret %s/%s", source.Key, secret.Namespace, secret.Name)
136+
}
137+
return string(value), nil
138+
}
139+
140+
func (a *apiTranslator) translateToolServerConfig(ctx context.Context, config v1alpha1.ToolServerConfig, namespace string) (string, *api.ToolServerConfig, error) {
74141
switch {
75142
case config.Stdio != nil:
143+
env := make(map[string]string)
144+
145+
if config.Stdio.Env != nil {
146+
for k, v := range config.Stdio.Env {
147+
env[k] = v
148+
}
149+
}
150+
151+
if len(config.Stdio.EnvFrom) > 0 {
152+
for _, envVar := range config.Stdio.EnvFrom {
153+
if envVar.ValueFrom != nil {
154+
value, err := a.resolveValueSource(ctx, envVar.ValueFrom, namespace)
155+
156+
if err != nil {
157+
return "", nil, fmt.Errorf("failed to resolve environment variable %s: %v", envVar.Name, err)
158+
}
159+
160+
env[envVar.Name] = value
161+
} else if envVar.Value != "" {
162+
env[envVar.Name] = envVar.Value
163+
}
164+
}
165+
}
166+
76167
return "kagent.tool_servers.StdioMcpToolServer", &api.ToolServerConfig{
77168
StdioMcpServerConfig: &api.StdioMcpServerConfig{
78169
Command: config.Stdio.Command,
79170
Args: config.Stdio.Args,
80-
Env: config.Stdio.Env,
171+
Env: env,
81172
},
82173
}, nil
83174
case config.Sse != nil:
84175
headers, err := convertMapFromAnytype(config.Sse.Headers)
85176
if err != nil {
86177
return "", nil, err
87178
}
179+
180+
if len(config.Sse.HeadersFrom) > 0 {
181+
for _, header := range config.Sse.HeadersFrom {
182+
if header.ValueFrom != nil {
183+
value, err := a.resolveValueSource(ctx, header.ValueFrom, namespace)
184+
185+
if err != nil {
186+
return "", nil, fmt.Errorf("failed to resolve header %s: %v", header.Name, err)
187+
}
188+
189+
headers[header.Name] = value
190+
} else if header.Value != "" {
191+
headers[header.Name] = header.Value
192+
}
193+
}
194+
}
195+
88196
timeout, err := convertDurationToSeconds(config.Sse.Timeout)
89197
if err != nil {
90198
return "", nil, err

0 commit comments

Comments
 (0)