11package handlers
22
33import (
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