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