Skip to content

Commit e3d2bde

Browse files
authored
Merge branch 'main' into feat/sort-sessions-by-last-activity
2 parents 639ed77 + f3ac0cf commit e3d2bde

5 files changed

Lines changed: 394 additions & 13 deletions

File tree

go/api/httpapi/types.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,28 @@ type ModelConfigResource struct {
4949
Status v1alpha2.ModelConfigStatus `json:"status,omitempty"`
5050
}
5151

52+
// SecretMaterial describes a Secret key/value pair to create or update alongside a ModelConfig.
53+
type SecretMaterial struct {
54+
Name string `json:"name"`
55+
Key string `json:"key"`
56+
Value string `json:"value"`
57+
}
58+
5259
// CreateModelConfigRequest is a thin wrapper: ref + optional inline apiKey + full CRD spec.
5360
type CreateModelConfigRequest struct {
54-
Ref string `json:"ref"`
55-
APIKey string `json:"apiKey,omitempty"`
56-
Spec v1alpha2.ModelConfigSpec `json:"spec"`
61+
Ref string `json:"ref"`
62+
// APIKey is an optional inline API key to store in a generated Secret.
63+
APIKey string `json:"apiKey,omitempty"`
64+
// Secrets are optional companion Secrets to create or update alongside the ModelConfig.
65+
Secrets []SecretMaterial `json:"secrets,omitempty"`
66+
Spec v1alpha2.ModelConfigSpec `json:"spec"`
5767
}
5868

5969
// UpdateModelConfigRequest is a thin wrapper: optional inline apiKey + full CRD spec.
6070
type UpdateModelConfigRequest struct {
61-
APIKey *string `json:"apiKey,omitempty"`
62-
Spec v1alpha2.ModelConfigSpec `json:"spec"`
71+
APIKey *string `json:"apiKey,omitempty"`
72+
Spec v1alpha2.ModelConfigSpec `json:"spec"`
73+
Secrets []SecretMaterial `json:"secrets,omitempty"`
6374
}
6475

6576
// Agent types

go/core/internal/httpserver/handlers/modelconfig.go

Lines changed: 134 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package handlers
22

33
import (
4+
"context"
45
"encoding/json"
6+
stderrors "errors"
57
"fmt"
8+
"maps"
69
"net/http"
710
"strings"
811

@@ -11,9 +14,11 @@ import (
1114
"github.com/kagent-dev/kagent/go/core/internal/httpserver/errors"
1215
common "github.com/kagent-dev/kagent/go/core/internal/utils"
1316
"github.com/kagent-dev/kagent/go/core/pkg/auth"
17+
corev1 "k8s.io/api/core/v1"
1418
apierrors "k8s.io/apimachinery/pkg/api/errors"
1519
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1620
"k8s.io/apimachinery/pkg/types"
21+
"k8s.io/apimachinery/pkg/util/validation"
1722
"sigs.k8s.io/controller-runtime/pkg/client"
1823
ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
1924
)
@@ -133,6 +138,10 @@ func (h *ModelConfigHandler) HandleCreateModelConfig(w ErrorResponseWriter, r *h
133138
w.RespondWithError(errors.NewBadRequestError(err.Error(), err))
134139
return
135140
}
141+
if err := validateSecretMaterials(req.Secrets); err != nil {
142+
w.RespondWithError(errors.NewBadRequestError(err.Error(), err))
143+
return
144+
}
136145

137146
log.V(1).Info("Checking if ModelConfig already exists")
138147
existingConfig := &v1alpha2.ModelConfig{}
@@ -181,6 +190,12 @@ func (h *ModelConfigHandler) HandleCreateModelConfig(w ErrorResponseWriter, r *h
181190
}
182191
}
183192

193+
if err := createOrUpdateCompanionSecrets(r.Context(), h.KubeClient, modelConfig, req.Secrets); err != nil {
194+
log.Error(err, "Failed to create or update companion secrets")
195+
w.RespondWithError(companionSecretAPIError(err))
196+
return
197+
}
198+
184199
log.Info("Successfully created ModelConfig", "ref", modelConfigRef)
185200
RespondWithJSON(w, http.StatusCreated, api.NewResponse(modelConfigResource(modelConfig), "Successfully created ModelConfig", false))
186201
}
@@ -221,6 +236,10 @@ func (h *ModelConfigHandler) HandleUpdateModelConfig(w ErrorResponseWriter, r *h
221236
w.RespondWithError(errors.NewBadRequestError(err.Error(), err))
222237
return
223238
}
239+
if err := validateSecretMaterials(req.Secrets); err != nil {
240+
w.RespondWithError(errors.NewBadRequestError(err.Error(), err))
241+
return
242+
}
224243

225244
log.V(1).Info("Getting existing ModelConfig")
226245
modelConfig := &v1alpha2.ModelConfig{}
@@ -240,6 +259,13 @@ func (h *ModelConfigHandler) HandleUpdateModelConfig(w ErrorResponseWriter, r *h
240259
req.Spec.APIKeySecret = configName
241260
req.Spec.APIKeySecretKey = fmt.Sprintf("%s_API_KEY", strings.ToUpper(string(req.Spec.Provider)))
242261
}
262+
modelConfig.Spec = req.Spec
263+
if err := h.KubeClient.Update(r.Context(), modelConfig); err != nil {
264+
log.Error(err, "Failed to update ModelConfig resource")
265+
w.RespondWithError(errors.NewInternalServerError("Failed to update ModelConfig", err))
266+
return
267+
}
268+
243269
if req.APIKey != nil && *req.APIKey != "" && req.Spec.Provider != v1alpha2.ModelProviderOllama {
244270
log.V(1).Info("Updating API key secret")
245271
if err := createOrUpdateSecretWithOwnerReference(
@@ -254,10 +280,9 @@ func (h *ModelConfigHandler) HandleUpdateModelConfig(w ErrorResponseWriter, r *h
254280
log.V(1).Info("Successfully updated API key secret")
255281
}
256282

257-
modelConfig.Spec = req.Spec
258-
if err := h.KubeClient.Update(r.Context(), modelConfig); err != nil {
259-
log.Error(err, "Failed to update ModelConfig resource")
260-
w.RespondWithError(errors.NewInternalServerError("Failed to update ModelConfig", err))
283+
if err := createOrUpdateCompanionSecrets(r.Context(), h.KubeClient, modelConfig, req.Secrets); err != nil {
284+
log.Error(err, "Failed to create or update companion secrets")
285+
w.RespondWithError(companionSecretAPIError(err))
261286
return
262287
}
263288

@@ -324,3 +349,108 @@ func validateAPIKeySecretRef(apiKeySecret, apiKeySecretKey string, provider v1al
324349
}
325350
return nil
326351
}
352+
353+
func validateSecretMaterials(secrets []api.SecretMaterial) error {
354+
for _, secret := range secrets {
355+
if errs := validation.IsDNS1123Subdomain(secret.Name); len(errs) > 0 {
356+
return fmt.Errorf("invalid secret name %q: %s", secret.Name, strings.Join(errs, "; "))
357+
}
358+
if errs := validation.IsConfigMapKey(secret.Key); len(errs) > 0 {
359+
return fmt.Errorf("invalid key %q for secret %q: %s", secret.Key, secret.Name, strings.Join(errs, "; "))
360+
}
361+
}
362+
return nil
363+
}
364+
365+
var errInvalidCompanionSecret = stderrors.New("invalid companion secret")
366+
367+
// companionSecretAPIError returns an API error for companion secret validation errors.
368+
func companionSecretAPIError(err error) *errors.APIError {
369+
if stderrors.Is(err, errInvalidCompanionSecret) {
370+
return errors.NewBadRequestError(err.Error(), err)
371+
}
372+
return errors.NewInternalServerError("Failed to create or update companion secrets", err)
373+
}
374+
375+
func createOrUpdateCompanionSecrets(ctx context.Context, kubeClient client.Client, owner *v1alpha2.ModelConfig, secrets []api.SecretMaterial) error {
376+
// Group secrets by name and key.
377+
secretsByName := map[string]map[string][]byte{}
378+
for _, secret := range secrets {
379+
if _, ok := secretsByName[secret.Name]; !ok {
380+
secretsByName[secret.Name] = map[string][]byte{}
381+
}
382+
secretsByName[secret.Name][secret.Key] = []byte(secret.Value)
383+
}
384+
385+
namespace := owner.GetNamespace()
386+
for name, data := range secretsByName {
387+
existingSecret := &corev1.Secret{}
388+
// Get the existing secret by name and namespace.
389+
err := kubeClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, existingSecret)
390+
if err != nil {
391+
if !apierrors.IsNotFound(err) {
392+
return fmt.Errorf("failed to get companion secret %s/%s: %w", namespace, name, err)
393+
}
394+
395+
// Create the secret if it doesn't exist.
396+
secret := &corev1.Secret{
397+
ObjectMeta: metav1.ObjectMeta{
398+
Name: name,
399+
Namespace: namespace,
400+
OwnerReferences: []metav1.OwnerReference{modelConfigOwnerReference(owner)},
401+
},
402+
Type: corev1.SecretTypeOpaque,
403+
Data: data,
404+
}
405+
if err := kubeClient.Create(ctx, secret); err != nil {
406+
return fmt.Errorf("failed to create companion secret %s/%s: %w", namespace, name, err)
407+
}
408+
continue
409+
}
410+
411+
if existingSecret.Type != corev1.SecretTypeOpaque {
412+
return fmt.Errorf("%w: companion secret %s/%s must be type %q, got %q", errInvalidCompanionSecret, namespace, name, corev1.SecretTypeOpaque, existingSecret.Type)
413+
}
414+
if !isOwnedByModelConfig(existingSecret, owner) {
415+
return fmt.Errorf("%w: companion secret %s/%s is not managed by ModelConfig %s/%s", errInvalidCompanionSecret, namespace, name, owner.GetNamespace(), owner.GetName())
416+
}
417+
418+
if existingSecret.Data == nil {
419+
existingSecret.Data = map[string][]byte{}
420+
}
421+
maps.Copy(existingSecret.Data, data)
422+
if err := kubeClient.Update(ctx, existingSecret); err != nil {
423+
return fmt.Errorf("failed to update companion secret %s/%s: %w", namespace, name, err)
424+
}
425+
}
426+
427+
return nil
428+
}
429+
430+
// modelConfigOwnerReference returns the owner reference for the model config.
431+
func modelConfigOwnerReference(owner *v1alpha2.ModelConfig) metav1.OwnerReference {
432+
controller := true
433+
return metav1.OwnerReference{
434+
APIVersion: v1alpha2.GroupVersion.Identifier(),
435+
Kind: "ModelConfig",
436+
Name: owner.GetName(),
437+
UID: owner.GetUID(),
438+
Controller: &controller,
439+
}
440+
}
441+
442+
// isOwnedByModelConfig checks if the secret is owned by the model config.
443+
func isOwnedByModelConfig(secret *corev1.Secret, owner *v1alpha2.ModelConfig) bool {
444+
for _, ownerRef := range secret.GetOwnerReferences() {
445+
if ownerRef.APIVersion != v1alpha2.GroupVersion.Identifier() ||
446+
ownerRef.Kind != "ModelConfig" ||
447+
ownerRef.Name != owner.GetName() {
448+
continue
449+
}
450+
if owner.GetUID() != "" && ownerRef.UID != owner.GetUID() {
451+
continue
452+
}
453+
return true
454+
}
455+
return false
456+
}

0 commit comments

Comments
 (0)