@@ -60,6 +60,7 @@ func WorkloadRouter(
60
60
r .Post ("/restart" , routes .restartWorkloadsBulk )
61
61
r .Post ("/delete" , routes .deleteWorkloadsBulk )
62
62
r .Get ("/{name}" , routes .getWorkload )
63
+ r .Post ("/{name}/edit" , routes .updateWorkload )
63
64
r .Post ("/{name}/stop" , routes .stopWorkload )
64
65
r .Post ("/{name}/restart" , routes .restartWorkload )
65
66
r .Get ("/{name}/logs" , routes .getLogsForWorkload )
@@ -124,14 +125,15 @@ func (s *WorkloadRoutes) listWorkloads(w http.ResponseWriter, r *http.Request) {
124
125
// @Tags workloads
125
126
// @Produce json
126
127
// @Param name path string true "Workload name"
127
- // @Success 200 {object} core.Workload
128
+ // @Success 200 {object} createRequest
128
129
// @Failure 404 {string} string "Not Found"
129
130
// @Router /api/v1beta/workloads/{name} [get]
130
131
func (s * WorkloadRoutes ) getWorkload (w http.ResponseWriter , r * http.Request ) {
131
132
ctx := r .Context ()
132
133
name := chi .URLParam (r , "name" )
133
134
134
- workload , err := s .workloadManager .GetWorkload (ctx , name )
135
+ // Check if workload exists first
136
+ _ , err := s .workloadManager .GetWorkload (ctx , name )
135
137
if err != nil {
136
138
if errors .Is (err , runtime .ErrWorkloadNotFound ) {
137
139
http .Error (w , "Workload not found" , http .StatusNotFound )
@@ -145,10 +147,19 @@ func (s *WorkloadRoutes) getWorkload(w http.ResponseWriter, r *http.Request) {
145
147
return
146
148
}
147
149
148
- w . Header (). Set ( "Content-Type" , "application/json" )
149
- err = json . NewEncoder ( w ). Encode ( workload )
150
+ // Load the workload configuration
151
+ runConfig , err := runner . LoadState ( ctx , name )
150
152
if err != nil {
151
- http .Error (w , "Failed to marshal workload details" , http .StatusInternalServerError )
153
+ logger .Errorf ("Failed to load workload configuration for %s: %v" , name , err )
154
+ http .Error (w , "Workload configuration not found" , http .StatusNotFound )
155
+ return
156
+ }
157
+
158
+ config := runConfigToCreateRequest (runConfig )
159
+
160
+ w .Header ().Set ("Content-Type" , "application/json" )
161
+ if err := json .NewEncoder (w ).Encode (config ); err != nil {
162
+ http .Error (w , "Failed to marshal workload configuration" , http .StatusInternalServerError )
152
163
return
153
164
}
154
165
}
@@ -258,23 +269,6 @@ func (s *WorkloadRoutes) createWorkload(w http.ResponseWriter, r *http.Request)
258
269
return
259
270
}
260
271
261
- // Fetch or build the requested image
262
- // TODO: Make verification configurable and return errors over the API.
263
- imageURL , imageMetadata , err := retriever .GetMCPServer (
264
- ctx ,
265
- req .Image ,
266
- "" , // We do not let the user specify a CA cert path here.
267
- retriever .VerifyImageWarn ,
268
- )
269
- if err != nil {
270
- if errors .Is (err , retriever .ErrImageNotFound ) {
271
- http .Error (w , "MCP server image not found" , http .StatusNotFound )
272
- } else {
273
- http .Error (w , fmt .Sprintf ("Failed to retrieve MCP server image: %v" , err ), http .StatusInternalServerError )
274
- }
275
- return
276
- }
277
-
278
272
// check if the workload already exists
279
273
if req .Name != "" {
280
274
exists , err := s .workloadManager .DoesWorkloadExist (ctx , req .Name )
@@ -288,54 +282,92 @@ func (s *WorkloadRoutes) createWorkload(w http.ResponseWriter, r *http.Request)
288
282
}
289
283
}
290
284
291
- // NOTE: None of the k8s-related config logic is included here.
292
- runSecrets := secrets .SecretParametersToCLI (req .Secrets )
293
- runConfig , err := runner .NewRunConfigBuilder ().
294
- WithRuntime (s .containerRuntime ).
295
- WithCmdArgs (req .CmdArguments ).
296
- WithName (req .Name ).
297
- WithImage (imageURL ).
298
- WithHost (req .Host ).
299
- WithTargetHost (transport .LocalhostIPv4 ).
300
- WithDebug (s .debugMode ).
301
- WithVolumes (req .Volumes ).
302
- WithSecrets (runSecrets ).
303
- WithAuthzConfigPath (req .AuthzConfig ).
304
- WithAuditConfigPath ("" ).
305
- WithPermissionProfile (req .PermissionProfile ).
306
- WithNetworkIsolation (req .NetworkIsolation ).
307
- WithK8sPodPatch ("" ).
308
- WithProxyMode (types .ProxyMode (req .ProxyMode )).
309
- WithTransportAndPorts (req .Transport , 0 , req .TargetPort ).
310
- WithAuditEnabled (false , "" ).
311
- WithOIDCConfig (req .OIDC .Issuer , req .OIDC .Audience , req .OIDC .JwksURL , req .OIDC .ClientID ,
312
- "" , "" , "" , "" , "" , false ). // JWKS auth parameters not exposed through API yet
313
- WithTelemetryConfig ("" , false , "" , 0.0 , nil , false , nil ). // Not exposed through API yet.
314
- WithToolsFilter (req .ToolsFilter ).
315
- Build (ctx , imageMetadata , req .EnvVars , & runner.DetachedEnvVarValidator {})
285
+ // Create the workload using shared logic
286
+ runConfig , err := s .createWorkloadFromRequest (ctx , & req )
316
287
if err != nil {
317
- logger .Errorf ("Failed to create run config: %v" , err )
318
- http .Error (w , "Failed to create run config" , http .StatusBadRequest )
288
+ // Error messages already logged in createWorkloadFromRequest
289
+ if errors .Is (err , retriever .ErrImageNotFound ) || err .Error () == "MCP server image not found" {
290
+ http .Error (w , err .Error (), http .StatusNotFound )
291
+ } else {
292
+ http .Error (w , err .Error (), http .StatusInternalServerError )
293
+ }
319
294
return
320
295
}
321
296
322
- if err := runConfig .SaveState (ctx ); err != nil {
323
- logger .Errorf ("Failed to save workload config: %v" , err )
324
- http .Error (w , "Failed to save workload config" , http .StatusInternalServerError )
297
+ // Return name so that the client will get the auto-generated name.
298
+ w .Header ().Set ("Content-Type" , "application/json" )
299
+ w .WriteHeader (http .StatusCreated )
300
+ resp := createWorkloadResponse {
301
+ Name : runConfig .ContainerName ,
302
+ Port : runConfig .Port ,
303
+ }
304
+ if err = json .NewEncoder (w ).Encode (resp ); err != nil {
305
+ http .Error (w , "Failed to marshal workload details" , http .StatusInternalServerError )
325
306
return
326
307
}
308
+ }
309
+
310
+ // updateWorkload
311
+ //
312
+ // @Summary Update workload
313
+ // @Description Update an existing workload configuration
314
+ // @Tags workloads
315
+ // @Accept json
316
+ // @Produce json
317
+ // @Param name path string true "Workload name"
318
+ // @Param request body updateRequest true "Update workload request"
319
+ // @Success 200 {object} createWorkloadResponse
320
+ // @Failure 400 {string} string "Bad Request"
321
+ // @Failure 404 {string} string "Not Found"
322
+ // @Router /api/v1beta/workloads/{name}/edit [post]
323
+ func (s * WorkloadRoutes ) updateWorkload (w http.ResponseWriter , r * http.Request ) {
324
+ ctx := r .Context ()
325
+ name := chi .URLParam (r , "name" )
327
326
328
- // Start workload with specified RunConfig.
329
- err = s .workloadManager .RunWorkloadDetached (ctx , runConfig )
327
+ // Parse request body
328
+ var updateReq updateRequest
329
+ if err := json .NewDecoder (r .Body ).Decode (& updateReq ); err != nil {
330
+ http .Error (w , "Invalid JSON: " + err .Error (), http .StatusBadRequest )
331
+ return
332
+ }
333
+
334
+ // Check if workload exists
335
+ _ , err := s .workloadManager .GetWorkload (ctx , name )
330
336
if err != nil {
331
- logger .Errorf ("Failed to start workload: %v" , err )
332
- http .Error (w , "Failed to start workload " , http .StatusInternalServerError )
337
+ logger .Errorf ("Failed to get workload: %v" , err )
338
+ http .Error (w , "Workload not found " , http .StatusNotFound )
333
339
return
334
340
}
335
341
336
- // Return name so that the client will get the auto-generated name.
342
+ // Convert updateRequest to createRequest with the existing workload name
343
+ createReq := createRequest {
344
+ updateRequest : updateReq ,
345
+ Name : name , // Use the name from URL path, not from request body
346
+ }
347
+
348
+ // Stop the existing workload
349
+ if _ , err = s .workloadManager .StopWorkloads (ctx , []string {name }); err != nil {
350
+ logger .Errorf ("Failed to stop workload %s: %v" , name , err )
351
+ http .Error (w , "Failed to stop workload" , http .StatusInternalServerError )
352
+ return
353
+ }
354
+
355
+ // Delete the existing workload
356
+ if _ , err = s .workloadManager .DeleteWorkloads (ctx , []string {name }); err != nil {
357
+ logger .Errorf ("Failed to delete workload %s: %v" , name , err )
358
+ http .Error (w , "Failed to delete workload" , http .StatusInternalServerError )
359
+ return
360
+ }
361
+
362
+ // Create the new workload using shared logic
363
+ runConfig , err := s .createWorkloadFromRequest (ctx , & createReq )
364
+ if err != nil {
365
+ http .Error (w , err .Error (), http .StatusInternalServerError )
366
+ return
367
+ }
368
+
369
+ // Return the same response format as create
337
370
w .Header ().Set ("Content-Type" , "application/json" )
338
- w .WriteHeader (http .StatusCreated )
339
371
resp := createWorkloadResponse {
340
372
Name : runConfig .ContainerName ,
341
373
Port : runConfig .Port ,
@@ -560,12 +592,10 @@ type workloadListResponse struct {
560
592
Workloads []core.Workload `json:"workloads"`
561
593
}
562
594
563
- // createRequest represents the request to create a new workload
595
+ // updateRequest represents the request to update an existing workload
564
596
//
565
- // @Description Request to create a new workload
566
- type createRequest struct {
567
- // Name of the workload
568
- Name string `json:"name"`
597
+ // @Description Request to update an existing workload (name cannot be changed)
598
+ type updateRequest struct {
569
599
// Docker image to use
570
600
Image string `json:"image"`
571
601
// Host to bind to
@@ -596,6 +626,15 @@ type createRequest struct {
596
626
ToolsFilter []string `json:"tools"`
597
627
}
598
628
629
+ // createRequest represents the request to create a new workload
630
+ //
631
+ // @Description Request to create a new workload
632
+ type createRequest struct {
633
+ updateRequest
634
+ // Name of the workload
635
+ Name string `json:"name"`
636
+ }
637
+
599
638
// oidcOptions represents OIDC configuration options
600
639
//
601
640
// @Description OIDC configuration for workload authentication
@@ -670,3 +709,112 @@ func (s *WorkloadRoutes) getWorkloadNamesFromRequest(ctx context.Context, req bu
670
709
671
710
return workloadNames , nil
672
711
}
712
+
713
+ // createWorkloadFromRequest creates a workload from a request
714
+ func (s * WorkloadRoutes ) createWorkloadFromRequest (ctx context.Context , req * createRequest ) (* runner.RunConfig , error ) {
715
+ // Fetch or build the requested image
716
+ imageURL , imageMetadata , err := retriever .GetMCPServer (
717
+ ctx ,
718
+ req .Image ,
719
+ "" , // We do not let the user specify a CA cert path here.
720
+ retriever .VerifyImageWarn ,
721
+ )
722
+ if err != nil {
723
+ if errors .Is (err , retriever .ErrImageNotFound ) {
724
+ return nil , fmt .Errorf ("MCP server image not found" )
725
+ }
726
+ return nil , fmt .Errorf ("failed to retrieve MCP server image: %v" , err )
727
+ }
728
+
729
+ // Build RunConfig
730
+ runSecrets := secrets .SecretParametersToCLI (req .Secrets )
731
+ runConfig , err := runner .NewRunConfigBuilder ().
732
+ WithRuntime (s .containerRuntime ).
733
+ WithCmdArgs (req .CmdArguments ).
734
+ WithName (req .Name ).
735
+ WithImage (imageURL ).
736
+ WithHost (req .Host ).
737
+ WithTargetHost (transport .LocalhostIPv4 ).
738
+ WithDebug (s .debugMode ).
739
+ WithVolumes (req .Volumes ).
740
+ WithSecrets (runSecrets ).
741
+ WithAuthzConfigPath (req .AuthzConfig ).
742
+ WithAuditConfigPath ("" ).
743
+ WithPermissionProfile (req .PermissionProfile ).
744
+ WithNetworkIsolation (req .NetworkIsolation ).
745
+ WithK8sPodPatch ("" ).
746
+ WithProxyMode (types .ProxyMode (req .ProxyMode )).
747
+ WithTransportAndPorts (req .Transport , 0 , req .TargetPort ).
748
+ WithAuditEnabled (false , "" ).
749
+ WithOIDCConfig (req .OIDC .Issuer , req .OIDC .Audience , req .OIDC .JwksURL , req .OIDC .ClientID ,
750
+ "" , "" , "" , "" , "" , false ).
751
+ WithTelemetryConfig ("" , false , "" , 0.0 , nil , false , nil ).
752
+ WithToolsFilter (req .ToolsFilter ).
753
+ Build (ctx , imageMetadata , req .EnvVars , & runner.DetachedEnvVarValidator {})
754
+ if err != nil {
755
+ logger .Errorf ("Failed to build run config: %v" , err )
756
+ return nil , fmt .Errorf ("invalid configuration: %v" , err )
757
+ }
758
+
759
+ // Save the workload state
760
+ if err := runConfig .SaveState (ctx ); err != nil {
761
+ logger .Errorf ("Failed to save workload config: %v" , err )
762
+ return nil , fmt .Errorf ("failed to save workload config" )
763
+ }
764
+
765
+ // Start workload
766
+ if err := s .workloadManager .RunWorkloadDetached (ctx , runConfig ); err != nil {
767
+ logger .Errorf ("Failed to start workload: %v" , err )
768
+ return nil , fmt .Errorf ("failed to start workload" )
769
+ }
770
+
771
+ return runConfig , nil
772
+ }
773
+
774
+ // runConfigToCreateRequest converts a RunConfig to createRequest for API responses
775
+ func runConfigToCreateRequest (runConfig * runner.RunConfig ) * createRequest {
776
+ // Convert CLI secrets ([]string) back to SecretParameters
777
+ secretParams := make ([]secrets.SecretParameter , 0 , len (runConfig .Secrets ))
778
+ for _ , secretStr := range runConfig .Secrets {
779
+ // Parse the CLI format: "<name>,target=<target>"
780
+ if secretParam , err := secrets .ParseSecretParameter (secretStr ); err == nil {
781
+ secretParams = append (secretParams , secretParam )
782
+ }
783
+ // Ignore invalid secrets rather than failing the entire conversion
784
+ }
785
+
786
+ // Get OIDC fields from RunConfig
787
+ var oidcConfig oidcOptions
788
+ if runConfig .OIDCConfig != nil {
789
+ oidcConfig = oidcOptions {
790
+ Issuer : runConfig .OIDCConfig .Issuer ,
791
+ Audience : runConfig .OIDCConfig .Audience ,
792
+ JwksURL : runConfig .OIDCConfig .JWKSURL ,
793
+ IntrospectionURL : runConfig .OIDCConfig .IntrospectionURL ,
794
+ ClientID : runConfig .OIDCConfig .ClientID ,
795
+ ClientSecret : runConfig .OIDCConfig .ClientSecret ,
796
+ }
797
+ }
798
+
799
+ authzConfigPath := ""
800
+
801
+ return & createRequest {
802
+ updateRequest : updateRequest {
803
+ Image : runConfig .Image ,
804
+ Host : runConfig .Host ,
805
+ CmdArguments : runConfig .CmdArgs ,
806
+ TargetPort : runConfig .TargetPort ,
807
+ EnvVars : runConfig .EnvVars ,
808
+ Secrets : secretParams ,
809
+ Volumes : runConfig .Volumes ,
810
+ Transport : string (runConfig .Transport ),
811
+ AuthzConfig : authzConfigPath ,
812
+ OIDC : oidcConfig ,
813
+ PermissionProfile : runConfig .PermissionProfile ,
814
+ ProxyMode : string (runConfig .ProxyMode ),
815
+ NetworkIsolation : runConfig .IsolateNetwork ,
816
+ ToolsFilter : runConfig .ToolsFilter ,
817
+ },
818
+ Name : runConfig .Name ,
819
+ }
820
+ }
0 commit comments