@@ -347,10 +347,18 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
347
347
dbAutostartSchedule .String = * createWorkspace .AutostartSchedule
348
348
}
349
349
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
354
362
}
355
363
356
364
workspace , err := api .Database .GetWorkspaceByOwnerIDAndName (r .Context (), database.GetWorkspaceByOwnerIDAndNameParams {
@@ -559,14 +567,21 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
559
567
return
560
568
}
561
569
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
567
582
}
568
583
569
- err : = api .Database .UpdateWorkspaceTTL (r .Context (), database.UpdateWorkspaceTTLParams {
584
+ err = api .Database .UpdateWorkspaceTTL (r .Context (), database.UpdateWorkspaceTTLParams {
570
585
ID : workspace .ID ,
571
586
Ttl : dbTTL ,
572
587
})
@@ -590,36 +605,29 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
590
605
return
591
606
}
592
607
593
- var code = http .StatusOK
608
+ code := http .StatusOK
609
+ resp := httpapi.Response {}
594
610
595
611
err := api .Database .InTx (func (s database.Store ) error {
596
612
build , err := s .GetLatestWorkspaceBuildByWorkspaceID (r .Context (), workspace .ID )
597
613
if err != nil {
598
614
code = http .StatusInternalServerError
615
+ resp .Message = "workspace not found"
599
616
return xerrors .Errorf ("get latest workspace build: %w" , err )
600
617
}
601
618
602
619
if build .Transition != database .WorkspaceTransitionStart {
603
620
code = http .StatusConflict
621
+ resp .Message = "workspace must be started, current status: " + string (build .Transition )
604
622
return xerrors .Errorf ("workspace must be started, current status: %s" , build .Transition )
605
623
}
606
624
607
625
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 {
615
627
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
623
631
}
624
632
625
633
if err := s .UpdateWorkspaceBuildByID (r .Context (), database.UpdateWorkspaceBuildByIDParams {
@@ -628,15 +636,17 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
628
636
ProvisionerState : build .ProvisionerState ,
629
637
Deadline : newDeadline ,
630
638
}); err != nil {
639
+ code = http .StatusInternalServerError
640
+ resp .Message = "failed to extend workspace deadline"
631
641
return xerrors .Errorf ("update workspace build: %w" , err )
632
642
}
643
+ resp .Message = "deadline updated to " + newDeadline .Format (time .RFC3339 )
633
644
634
645
return nil
635
646
})
636
647
637
- var resp = httpapi.Response {}
638
648
if err != nil {
639
- resp . Message = err .Error ()
649
+ api . Logger . Info ( r . Context (), "extending workspace" , slog .Error (err ) )
640
650
}
641
651
httpapi .Write (rw , code , resp )
642
652
}
@@ -850,11 +860,44 @@ func convertSQLNullInt64(i sql.NullInt64) *time.Duration {
850
860
return (* time .Duration )(& i .Int64 )
851
861
}
852
862
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" )
857
900
}
858
901
859
- return true
902
+ return nil
860
903
}
0 commit comments