@@ -347,10 +347,18 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
347347 dbAutostartSchedule .String = * createWorkspace .AutostartSchedule
348348 }
349349
350- var dbTTL sql.NullInt64
351- if createWorkspace .TTL != nil && * createWorkspace .TTL > 0 {
352- dbTTL .Valid = true
353- dbTTL .Int64 = int64 (* createWorkspace .TTL )
350+ dbTTL , err := validWorkspaceTTL (createWorkspace .TTL )
351+ if err != nil {
352+ httpapi .Write (rw , http .StatusBadRequest , httpapi.Response {
353+ Message : "validate workspace ttl" ,
354+ Errors : []httpapi.Error {
355+ {
356+ Field : "ttl" ,
357+ Detail : err .Error (),
358+ },
359+ },
360+ })
361+ return
354362 }
355363
356364 workspace , err := api .Database .GetWorkspaceByOwnerIDAndName (r .Context (), database.GetWorkspaceByOwnerIDAndNameParams {
@@ -559,14 +567,21 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
559567 return
560568 }
561569
562- var dbTTL sql.NullInt64
563- if req .TTL != nil && * req .TTL > 0 {
564- truncated := req .TTL .Truncate (time .Minute )
565- dbTTL .Int64 = int64 (truncated )
566- dbTTL .Valid = true
570+ dbTTL , err := validWorkspaceTTL (req .TTL )
571+ if err != nil {
572+ httpapi .Write (rw , http .StatusBadRequest , httpapi.Response {
573+ Message : "validate workspace ttl" ,
574+ Errors : []httpapi.Error {
575+ {
576+ Field : "ttl" ,
577+ Detail : err .Error (),
578+ },
579+ },
580+ })
581+ return
567582 }
568583
569- err : = api .Database .UpdateWorkspaceTTL (r .Context (), database.UpdateWorkspaceTTLParams {
584+ err = api .Database .UpdateWorkspaceTTL (r .Context (), database.UpdateWorkspaceTTLParams {
570585 ID : workspace .ID ,
571586 Ttl : dbTTL ,
572587 })
@@ -590,36 +605,29 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
590605 return
591606 }
592607
593- var code = http .StatusOK
608+ code := http .StatusOK
609+ resp := httpapi.Response {}
594610
595611 err := api .Database .InTx (func (s database.Store ) error {
596612 build , err := s .GetLatestWorkspaceBuildByWorkspaceID (r .Context (), workspace .ID )
597613 if err != nil {
598614 code = http .StatusInternalServerError
615+ resp .Message = "workspace not found"
599616 return xerrors .Errorf ("get latest workspace build: %w" , err )
600617 }
601618
602619 if build .Transition != database .WorkspaceTransitionStart {
603620 code = http .StatusConflict
621+ resp .Message = "workspace must be started, current status: " + string (build .Transition )
604622 return xerrors .Errorf ("workspace must be started, current status: %s" , build .Transition )
605623 }
606624
607625 newDeadline := req .Deadline .UTC ()
608- if newDeadline .IsZero () {
609- // This should not be possible because the struct validation field enforces a non-zero value.
610- code = http .StatusBadRequest
611- return xerrors .New ("new deadline cannot be zero" )
612- }
613-
614- if newDeadline .Before (build .Deadline ) || newDeadline .Before (time .Now ()) {
626+ if err := validWorkspaceDeadline (build .Deadline , newDeadline ); err != nil {
615627 code = http .StatusBadRequest
616- return xerrors .Errorf ("new deadline %q must be after existing deadline %q" , newDeadline .Format (time .RFC3339 ), build .Deadline .Format (time .RFC3339 ))
617- }
618-
619- // Disallow updates within less than one minute
620- if withinDuration (newDeadline , build .Deadline , time .Minute ) {
621- code = http .StatusNotModified
622- return nil
628+ resp .Message = "bad extend workspace request"
629+ resp .Errors = append (resp .Errors , httpapi.Error {Field : "deadline" , Detail : err .Error ()})
630+ return err
623631 }
624632
625633 if err := s .UpdateWorkspaceBuildByID (r .Context (), database.UpdateWorkspaceBuildByIDParams {
@@ -628,15 +636,17 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
628636 ProvisionerState : build .ProvisionerState ,
629637 Deadline : newDeadline ,
630638 }); err != nil {
639+ code = http .StatusInternalServerError
640+ resp .Message = "failed to extend workspace deadline"
631641 return xerrors .Errorf ("update workspace build: %w" , err )
632642 }
643+ resp .Message = "deadline updated to " + newDeadline .Format (time .RFC3339 )
633644
634645 return nil
635646 })
636647
637- var resp = httpapi.Response {}
638648 if err != nil {
639- resp . Message = err .Error ()
649+ api . Logger . Info ( r . Context (), "extending workspace" , slog .Error (err ) )
640650 }
641651 httpapi .Write (rw , code , resp )
642652}
@@ -850,11 +860,44 @@ func convertSQLNullInt64(i sql.NullInt64) *time.Duration {
850860 return (* time .Duration )(& i .Int64 )
851861}
852862
853- func withinDuration (t1 , t2 time.Time , d time.Duration ) bool {
854- dt := t1 .Sub (t2 )
855- if dt < - d || dt > d {
856- return false
863+ func validWorkspaceTTL (ttl * time.Duration ) (sql.NullInt64 , error ) {
864+ if ttl == nil {
865+ return sql.NullInt64 {}, nil
866+ }
867+
868+ truncated := ttl .Truncate (time .Minute )
869+ if truncated < time .Minute {
870+ return sql.NullInt64 {}, xerrors .New ("ttl must be at least one minute" )
871+ }
872+
873+ if truncated > 24 * 7 * time .Hour {
874+ return sql.NullInt64 {}, xerrors .New ("ttl must be less than 7 days" )
875+ }
876+
877+ return sql.NullInt64 {
878+ Valid : true ,
879+ Int64 : int64 (truncated ),
880+ }, nil
881+ }
882+
883+ func validWorkspaceDeadline (old , new time.Time ) error {
884+ if old .IsZero () {
885+ return xerrors .New ("nothing to do: no existing deadline set" )
886+ }
887+
888+ now := time .Now ()
889+ if new .Before (now ) {
890+ return xerrors .New ("new deadline must be in the future" )
891+ }
892+
893+ delta := new .Sub (old )
894+ if delta < time .Minute {
895+ return xerrors .New ("minimum extension is one minute" )
896+ }
897+
898+ if delta > 24 * time .Hour {
899+ return xerrors .New ("maximum extension is 24 hours" )
857900 }
858901
859- return true
902+ return nil
860903}
0 commit comments