@@ -2,23 +2,32 @@ package autobuild_test
2
2
3
3
import (
4
4
"context"
5
+ "database/sql"
6
+ "fmt"
5
7
"os"
8
+ "sync/atomic"
6
9
"testing"
7
10
"time"
8
11
9
12
"github.com/google/uuid"
10
13
"github.com/stretchr/testify/assert"
11
14
"github.com/stretchr/testify/require"
12
15
"go.uber.org/goleak"
16
+ "golang.org/x/xerrors"
17
+
18
+ "cdr.dev/slog/sloggers/slogtest"
13
19
14
20
"github.com/coder/coder/coderd/autobuild"
15
21
"github.com/coder/coder/coderd/coderdtest"
16
22
"github.com/coder/coder/coderd/database"
23
+ "github.com/coder/coder/coderd/database/dbgen"
24
+ "github.com/coder/coder/coderd/database/dbtestutil"
17
25
"github.com/coder/coder/coderd/schedule"
18
26
"github.com/coder/coder/coderd/util/ptr"
19
27
"github.com/coder/coder/codersdk"
20
28
"github.com/coder/coder/provisioner/echo"
21
29
"github.com/coder/coder/provisionersdk/proto"
30
+ "github.com/coder/coder/testutil"
22
31
)
23
32
24
33
func TestExecutorAutostartOK (t * testing.T ) {
@@ -648,6 +657,151 @@ func TestExecutorAutostartTemplateDisabled(t *testing.T) {
648
657
assert .Len (t , stats .Transitions , 0 )
649
658
}
650
659
660
+ // TesetExecutorFailedWorkspace tests that failed workspaces that breach
661
+ // their template failed_ttl threshold trigger a stop job.
662
+ func TestExecutorFailedWorkspace (t * testing.T ) {
663
+ t .Parallel ()
664
+
665
+ t .Run ("AGPLOK" , func (t * testing.T ) {
666
+ t .Parallel ()
667
+
668
+ var (
669
+ db , _ = dbtestutil .NewDB (t )
670
+ logger = slogtest .Make (t , nil )
671
+ templateStore = schedule .NewAGPLTemplateScheduleStore ()
672
+ user = dbgen .User (t , db , database.User {})
673
+ org = dbgen .Organization (t , db , database.Organization {})
674
+ template = dbgen .Template (t , db , database.Template {
675
+ FailureTTL : int64 (time .Minute ),
676
+ OrganizationID : org .ID ,
677
+ CreatedBy : user .ID ,
678
+ })
679
+ version = dbgen .TemplateVersion (t , db , database.TemplateVersion {
680
+ TemplateID : uuid.NullUUID {
681
+ UUID : template .ID ,
682
+ Valid : true ,
683
+ },
684
+ OrganizationID : org .ID ,
685
+ CreatedBy : user .ID ,
686
+ })
687
+ ws = dbgen .Workspace (t , db , database.Workspace {
688
+ TemplateID : template .ID ,
689
+ OwnerID : user .ID ,
690
+ OrganizationID : org .ID ,
691
+ })
692
+ job = dbgen .ProvisionerJob (t , db , database.ProvisionerJob {
693
+ OrganizationID : org .ID ,
694
+ CompletedAt : sql.NullTime {
695
+ Valid : true ,
696
+ Time : time .Now ().Add (- time .Hour ),
697
+ },
698
+ Error : sql.NullString {
699
+ Valid : true ,
700
+ String : "failed!" ,
701
+ },
702
+ })
703
+
704
+ _ = dbgen .WorkspaceBuild (t , db , database.WorkspaceBuild {
705
+ WorkspaceID : ws .ID ,
706
+ JobID : job .ID ,
707
+ TemplateVersionID : version .ID ,
708
+ })
709
+ ptr = atomic.Pointer [schedule.TemplateScheduleStore ]{}
710
+ ticker = make (chan time.Time )
711
+ ctx = testutil .Context (t , testutil .WaitMedium )
712
+ statCh = make (chan autobuild.Stats )
713
+ )
714
+ ptr .Store (& templateStore )
715
+ executor := autobuild .NewExecutor (ctx , db , & ptr , logger , ticker ).WithStatsChannel (statCh )
716
+ executor .Run ()
717
+ ticker <- time .Now ()
718
+ stats := <- statCh
719
+ require .NoError (t , stats .Error )
720
+ require .Len (t , stats .Transitions , 0 )
721
+ })
722
+
723
+ t .Run ("EnterpriseOK" , func (t * testing.T ) {
724
+ t .Parallel ()
725
+
726
+ var (
727
+ db , _ = dbtestutil .NewDB (t )
728
+ logger = slogtest .Make (t , nil )
729
+ templateStore schedule.TemplateScheduleStore = & enterpriseTemplateScheduleStore {}
730
+ user = dbgen .User (t , db , database.User {})
731
+ org = dbgen .Organization (t , db , database.Organization {})
732
+ versionJob = dbgen .ProvisionerJob (t , db , database.ProvisionerJob {
733
+ OrganizationID : org .ID ,
734
+ CompletedAt : sql.NullTime {
735
+ Valid : true ,
736
+ Time : time .Now ().Add (- time .Hour ),
737
+ },
738
+ StartedAt : sql.NullTime {
739
+ Valid : true ,
740
+ Time : time .Now ().Add (- time .Hour * 2 ),
741
+ },
742
+ })
743
+ template = dbgen .Template (t , db , database.Template {
744
+ OrganizationID : org .ID ,
745
+ CreatedBy : user .ID ,
746
+ })
747
+ version = dbgen .TemplateVersion (t , db , database.TemplateVersion {
748
+ TemplateID : uuid.NullUUID {
749
+ UUID : template .ID ,
750
+ Valid : true ,
751
+ },
752
+ OrganizationID : org .ID ,
753
+ CreatedBy : user .ID ,
754
+ JobID : versionJob .ID ,
755
+ })
756
+ ws = dbgen .Workspace (t , db , database.Workspace {
757
+ TemplateID : template .ID ,
758
+ OwnerID : user .ID ,
759
+ OrganizationID : org .ID ,
760
+ })
761
+ job = dbgen .ProvisionerJob (t , db , database.ProvisionerJob {
762
+ OrganizationID : org .ID ,
763
+ StartedAt : sql.NullTime {
764
+ Valid : true ,
765
+ Time : time .Now ().Add (- time .Hour * 2 ),
766
+ },
767
+ CompletedAt : sql.NullTime {
768
+ Valid : true ,
769
+ Time : time .Now ().Add (- time .Hour ),
770
+ },
771
+ Error : sql.NullString {
772
+ Valid : true ,
773
+ String : "failed!" ,
774
+ },
775
+ })
776
+
777
+ _ = dbgen .WorkspaceBuild (t , db , database.WorkspaceBuild {
778
+ WorkspaceID : ws .ID ,
779
+ JobID : job .ID ,
780
+ TemplateVersionID : version .ID ,
781
+ })
782
+ ptr = atomic.Pointer [schedule.TemplateScheduleStore ]{}
783
+ ticker = make (chan time.Time )
784
+ ctx = testutil .Context (t , testutil .WaitMedium )
785
+ statCh = make (chan autobuild.Stats )
786
+ )
787
+ fmt .Printf ("provisionerjob: %s\n " , job .ID )
788
+ _ , err := templateStore .SetTemplateScheduleOptions (ctx , db , template , schedule.TemplateScheduleOptions {
789
+ FailureTTL : time .Minute ,
790
+ })
791
+ require .NoError (t , err )
792
+ ptr .Store (& templateStore )
793
+ executor := autobuild .NewExecutor (ctx , db , & ptr , logger , ticker ).WithStatsChannel (statCh )
794
+ executor .Run ()
795
+ ticker <- time .Now ()
796
+ stats := <- statCh
797
+ require .NoError (t , stats .Error )
798
+ require .Len (t , stats .Transitions , 1 )
799
+ trans := stats .Transitions [ws .ID ]
800
+ require .Equal (t , database .WorkspaceTransitionStop , trans )
801
+ })
802
+
803
+ }
804
+
651
805
func mustProvisionWorkspace (t * testing.T , client * codersdk.Client , mut ... func (* codersdk.CreateWorkspaceRequest )) codersdk.Workspace {
652
806
t .Helper ()
653
807
user := coderdtest .CreateFirstUser (t , client )
@@ -705,3 +859,71 @@ func mustWorkspaceParameters(t *testing.T, client *codersdk.Client, workspaceID
705
859
func TestMain (m * testing.M ) {
706
860
goleak .VerifyTestMain (m )
707
861
}
862
+
863
+ type enterpriseTemplateScheduleStore struct {}
864
+
865
+ var _ schedule.TemplateScheduleStore = & enterpriseTemplateScheduleStore {}
866
+
867
+ func (* enterpriseTemplateScheduleStore ) GetTemplateScheduleOptions (ctx context.Context , db database.Store , templateID uuid.UUID ) (schedule.TemplateScheduleOptions , error ) {
868
+ tpl , err := db .GetTemplateByID (ctx , templateID )
869
+ if err != nil {
870
+ return schedule.TemplateScheduleOptions {}, err
871
+ }
872
+
873
+ return schedule.TemplateScheduleOptions {
874
+ UserAutostartEnabled : tpl .AllowUserAutostart ,
875
+ UserAutostopEnabled : tpl .AllowUserAutostop ,
876
+ DefaultTTL : time .Duration (tpl .DefaultTTL ),
877
+ MaxTTL : time .Duration (tpl .MaxTTL ),
878
+ FailureTTL : time .Duration (tpl .FailureTTL ),
879
+ InactivityTTL : time .Duration (tpl .InactivityTTL ),
880
+ }, nil
881
+ }
882
+
883
+ func (* enterpriseTemplateScheduleStore ) SetTemplateScheduleOptions (ctx context.Context , db database.Store , tpl database.Template , opts schedule.TemplateScheduleOptions ) (database.Template , error ) {
884
+ if int64 (opts .DefaultTTL ) == tpl .DefaultTTL &&
885
+ int64 (opts .MaxTTL ) == tpl .MaxTTL &&
886
+ int64 (opts .FailureTTL ) == tpl .FailureTTL &&
887
+ int64 (opts .InactivityTTL ) == tpl .InactivityTTL &&
888
+ opts .UserAutostartEnabled == tpl .AllowUserAutostart &&
889
+ opts .UserAutostopEnabled == tpl .AllowUserAutostop {
890
+ // Avoid updating the UpdatedAt timestamp if nothing will be changed.
891
+ return tpl , nil
892
+ }
893
+
894
+ template , err := db .UpdateTemplateScheduleByID (ctx , database.UpdateTemplateScheduleByIDParams {
895
+ ID : tpl .ID ,
896
+ UpdatedAt : database .Now (),
897
+ AllowUserAutostart : opts .UserAutostartEnabled ,
898
+ AllowUserAutostop : opts .UserAutostopEnabled ,
899
+ DefaultTTL : int64 (opts .DefaultTTL ),
900
+ MaxTTL : int64 (opts .MaxTTL ),
901
+ FailureTTL : int64 (opts .FailureTTL ),
902
+ InactivityTTL : int64 (opts .InactivityTTL ),
903
+ })
904
+ if err != nil {
905
+ return database.Template {}, xerrors .Errorf ("update template schedule: %w" , err )
906
+ }
907
+
908
+ // Update all workspaces using the template to set the user defined schedule
909
+ // to be within the new bounds. This essentially does the following for each
910
+ // workspace using the template.
911
+ // if (template.ttl != NULL) {
912
+ // workspace.ttl = min(workspace.ttl, template.ttl)
913
+ // }
914
+ //
915
+ // NOTE: this does not apply to currently running workspaces as their
916
+ // schedule information is committed to the workspace_build during start.
917
+ // This limitation is displayed to the user while editing the template.
918
+ if opts .MaxTTL > 0 {
919
+ err = db .UpdateWorkspaceTTLToBeWithinTemplateMax (ctx , database.UpdateWorkspaceTTLToBeWithinTemplateMaxParams {
920
+ TemplateID : template .ID ,
921
+ TemplateMaxTTL : int64 (opts .MaxTTL ),
922
+ })
923
+ if err != nil {
924
+ return database.Template {}, xerrors .Errorf ("update TTL of all workspaces on template to be within new template max TTL: %w" , err )
925
+ }
926
+ }
927
+
928
+ return template , nil
929
+ }
0 commit comments