@@ -35,6 +35,8 @@ import (
3535
3636type WorkspaceCreateEnvelope Envelope [* models.WorkspaceCreate ]
3737
38+ type WorkspaceUpdateEnvelope Envelope [* models.WorkspaceUpdate ]
39+
3840type WorkspaceListEnvelope Envelope [[]models.Workspace ]
3941
4042type WorkspaceEnvelope Envelope [models.Workspace ]
@@ -292,6 +294,135 @@ func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps
292294 a .createdResponse (w , r , responseEnvelope , location )
293295}
294296
297+ // UpdateWorkspaceHandler updates an existing workspace.
298+ //
299+ // @Summary Update workspace
300+ // @Description Updates an existing workspace
301+ // @Tags workspaces
302+ // @ID updateWorkspace
303+ // @Accept json
304+ // @Produce json
305+ // @Param namespace path string true "Namespace of the workspace" extensions(x-example=kubeflow-user-example-com)
306+ // @Param name path string true "Name of the workspace" extensions(x-example=my-workspace)
307+ // @Param body body WorkspaceCreateEnvelope true "Workspace creation configuration"
308+ // @Success 200 {object} WorkspaceEnvelope "Workspace updated successfully"
309+ // @Failure 400 {object} ErrorEnvelope "Bad Request."
310+ // @Failure 401 {object} ErrorEnvelope "Unauthorized. Authentication is required."
311+ // @Failure 403 {object} ErrorEnvelope "Forbidden. User does not have permission to create workspace."
312+ // @Failure 409 {object} ErrorEnvelope "Conflict. Current workspace generation is newer than provided."
313+ // @Failure 413 {object} ErrorEnvelope "Request Entity Too Large. The request body is too large."
314+ // @Failure 415 {object} ErrorEnvelope "Unsupported Media Type. Content-Type header is not correct."
315+ // @Failure 422 {object} ErrorEnvelope "Unprocessable Entity. Validation error."
316+ // @Failure 500 {object} ErrorEnvelope "Internal server error. An unexpected error occurred on the server."
317+ // @Router /workspaces/{namespace}/{name} [patch]
318+ func (a * App ) UpdateWorkspaceHandler (w http.ResponseWriter , r * http.Request , ps httprouter.Params ) {
319+ namespace := ps .ByName (NamespacePathParam )
320+ workspaceName := ps .ByName (ResourceNamePathParam )
321+
322+ // validate path parameters
323+ var valErrs field.ErrorList
324+ valErrs = append (valErrs , helper .ValidateKubernetesNamespaceName (field .NewPath (NamespacePathParam ), namespace )... )
325+ valErrs = append (valErrs , helper .ValidateWorkspaceName (field .NewPath (ResourceNamePathParam ), workspaceName )... )
326+ if len (valErrs ) > 0 {
327+ a .failedValidationResponse (w , r , errMsgPathParamsInvalid , valErrs , nil )
328+ return
329+ }
330+
331+ //
332+ // TODO: require the caller to provide the generation base that they are expecting to update.
333+ // either include the generation as a field in the request body, or a header.
334+ //
335+
336+ //
337+ // TODO: update the "get" workspace (not list) to return the current generation of the workspace.
338+ // this probably means swapping the GET for Workspace with return a WorkspaceUpdate rather than a Workspace
339+ // - it also means that we will be treating GET as only used for retrieving the current state of the workspace,
340+ // before updating it, whereas LIST is used for getting the details of all workspaces (e.g. the table view in the UI).
341+ //
342+
343+ // validate the Content-Type header
344+ if success := a .ValidateContentType (w , r , "application/json" ); ! success {
345+ return
346+ }
347+
348+ // decode the request body
349+ bodyEnvelope := & WorkspaceUpdateEnvelope {}
350+ err := a .DecodeJSON (r , bodyEnvelope )
351+ if err != nil {
352+ if a .IsMaxBytesError (err ) {
353+ a .requestEntityTooLargeResponse (w , r , err )
354+ return
355+ }
356+ //
357+ // TODO: handle UnmarshalTypeError and return 422,
358+ // decode the paths which were failed to decode (included in the error)
359+ // and also do this in the other handlers which decode json
360+ //
361+ a .badRequestResponse (w , r , fmt .Errorf ("error decoding request body: %w" , err ))
362+ return
363+ }
364+
365+ // validate the request body
366+ dataPath := field .NewPath ("data" )
367+ if bodyEnvelope .Data == nil {
368+ valErrs = field.ErrorList {field .Required (dataPath , "data is required" )}
369+ a .failedValidationResponse (w , r , errMsgRequestBodyInvalid , valErrs , nil )
370+ return
371+ }
372+ valErrs = bodyEnvelope .Data .Validate (dataPath )
373+ if len (valErrs ) > 0 {
374+ a .failedValidationResponse (w , r , errMsgRequestBodyInvalid , valErrs , nil )
375+ return
376+ }
377+
378+ workspaceUpdate := bodyEnvelope .Data
379+
380+ // =========================== AUTH ===========================
381+ authPolicies := []* auth.ResourcePolicy {
382+ auth .NewResourcePolicy (
383+ auth .ResourceVerbUpdate ,
384+ & kubefloworgv1beta1.Workspace {
385+ ObjectMeta : metav1.ObjectMeta {
386+ Namespace : namespace ,
387+ Name : workspaceName ,
388+ },
389+ },
390+ ),
391+ }
392+ if success := a .requireAuth (w , r , authPolicies ); ! success {
393+ return
394+ }
395+ // ============================================================
396+
397+ updatedWorkspace , err := a .repositories .Workspace .UpdateWorkspace (r .Context (), workspaceUpdate , namespace , workspaceName )
398+ if err != nil {
399+ if errors .Is (err , repository .ErrWorkspaceNotFound ) {
400+ a .notFoundResponse (w , r )
401+ return
402+ }
403+ //
404+ // TODO: there is still a race condition once we actually try and patch/update the workspace,
405+ // unless we use optimistic locking with the generation field
406+ // (or resourceVersion, with the risk of unexpected 500 errors to the caller).
407+ //
408+ if errors .Is (err , repository .ErrWorkspaceGenerationConflict ) {
409+ causes := helper .StatusCausesFromAPIStatus (err )
410+ a .conflictResponse (w , r , err , causes )
411+ return
412+ }
413+ if apierrors .IsInvalid (err ) {
414+ causes := helper .StatusCausesFromAPIStatus (err )
415+ a .failedValidationResponse (w , r , errMsgKubernetesValidation , nil , causes )
416+ return
417+ }
418+ a .serverErrorResponse (w , r , fmt .Errorf ("error updating workspace: %w" , err ))
419+ return
420+ }
421+
422+ responseEnvelope := & WorkspaceUpdateEnvelope {Data : updatedWorkspace }
423+ a .dataResponse (w , r , responseEnvelope )
424+ }
425+
295426// DeleteWorkspaceHandler deletes a specific workspace by namespace and name.
296427//
297428// @Summary Delete workspace
0 commit comments