Skip to content

Commit 4502851

Browse files
committedJun 19, 2023
add test
1 parent d622a4d commit 4502851

File tree

4 files changed

+243
-15
lines changed

4 files changed

+243
-15
lines changed
 

‎coderd/autobuild/lifecycle_executor_test.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,32 @@ package autobuild_test
22

33
import (
44
"context"
5+
"database/sql"
6+
"fmt"
57
"os"
8+
"sync/atomic"
69
"testing"
710
"time"
811

912
"github.com/google/uuid"
1013
"github.com/stretchr/testify/assert"
1114
"github.com/stretchr/testify/require"
1215
"go.uber.org/goleak"
16+
"golang.org/x/xerrors"
17+
18+
"cdr.dev/slog/sloggers/slogtest"
1319

1420
"github.com/coder/coder/coderd/autobuild"
1521
"github.com/coder/coder/coderd/coderdtest"
1622
"github.com/coder/coder/coderd/database"
23+
"github.com/coder/coder/coderd/database/dbgen"
24+
"github.com/coder/coder/coderd/database/dbtestutil"
1725
"github.com/coder/coder/coderd/schedule"
1826
"github.com/coder/coder/coderd/util/ptr"
1927
"github.com/coder/coder/codersdk"
2028
"github.com/coder/coder/provisioner/echo"
2129
"github.com/coder/coder/provisionersdk/proto"
30+
"github.com/coder/coder/testutil"
2231
)
2332

2433
func TestExecutorAutostartOK(t *testing.T) {
@@ -648,6 +657,151 @@ func TestExecutorAutostartTemplateDisabled(t *testing.T) {
648657
assert.Len(t, stats.Transitions, 0)
649658
}
650659

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+
651805
func mustProvisionWorkspace(t *testing.T, client *codersdk.Client, mut ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace {
652806
t.Helper()
653807
user := coderdtest.CreateFirstUser(t, client)
@@ -705,3 +859,71 @@ func mustWorkspaceParameters(t *testing.T, client *codersdk.Client, workspaceID
705859
func TestMain(m *testing.M) {
706860
goleak.VerifyTestMain(m)
707861
}
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+
}

‎coderd/autobuild/notify/notifier_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"go.uber.org/atomic"
1010
"go.uber.org/goleak"
1111

12-
"github.com/coder/coder/coderd/wsactions/notify"
12+
"github.com/coder/coder/coderd/autobuild/notify"
1313
)
1414

1515
func TestNotifier(t *testing.T) {

‎coderd/database/dbauthz/dbauthz.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,8 +1677,8 @@ func (q *querier) GetWorkspaces(ctx context.Context, arg database.GetWorkspacesP
16771677
return q.db.GetAuthorizedWorkspaces(ctx, arg, prep)
16781678
}
16791679

1680-
func (q *querier) GetWorkspacesEligibleForAutoStartStop(ctx context.Context, now time.Time) ([]database.Workspace, error) {
1681-
return q.db.GetWorkspacesEligibleForAutoStartStop(ctx, now)
1680+
func (q *querier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.Workspace, error) {
1681+
return q.db.GetWorkspacesEligibleForTransition(ctx, now)
16821682
}
16831683

16841684
func (q *querier) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {

‎coderd/wsbuilder/wsbuilder.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -617,11 +617,12 @@ func (b *Builder) authorize(authFunc func(action rbac.Action, object rbac.Object
617617
case database.WorkspaceTransitionStart, database.WorkspaceTransitionStop:
618618
action = rbac.ActionUpdate
619619
default:
620-
return BuildError{http.StatusBadRequest, fmt.Sprintf("Transition %q not supported.", b.trans), xerrors.New("")}
620+
msg := fmt.Sprintf("Transition %q not supported.", b.trans)
621+
return BuildError{http.StatusBadRequest, msg, xerrors.New(msg)}
621622
}
622623
if !authFunc(action, b.workspace) {
623624
// We use the same wording as the httpapi to avoid leaking the existence of the workspace
624-
return BuildError{http.StatusNotFound, httpapi.ResourceNotFoundResponse.Message, xerrors.New("")}
625+
return BuildError{http.StatusNotFound, httpapi.ResourceNotFoundResponse.Message, xerrors.New(httpapi.ResourceNotFoundResponse.Message)}
625626
}
626627

627628
template, err := b.getTemplate()
@@ -633,15 +634,15 @@ func (b *Builder) authorize(authFunc func(action rbac.Action, object rbac.Object
633634
// cloud state.
634635
if b.state.explicit != nil || b.state.orphan {
635636
if !authFunc(rbac.ActionUpdate, template.RBACObject()) {
636-
return BuildError{http.StatusForbidden, "Only template managers may provide custom state", xerrors.New("")}
637+
return BuildError{http.StatusForbidden, "Only template managers may provide custom state", xerrors.New("Only template managers may provide custom state")}
637638
}
638639
}
639640

640641
if b.logLevel != "" && !authFunc(rbac.ActionUpdate, template) {
641642
return BuildError{
642643
http.StatusBadRequest,
643644
"Workspace builds with a custom log level are restricted to template authors only.",
644-
xerrors.New(""),
645+
xerrors.New("Workspace builds with a custom log level are restricted to template authors only."),
645646
}
646647
}
647648
return nil
@@ -686,22 +687,26 @@ func (b *Builder) checkTemplateJobStatus() error {
686687
templateVersionJobStatus := db2sdk.ProvisionerJobStatus(*templateVersionJob)
687688
switch templateVersionJobStatus {
688689
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
690+
msg := fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus)
691+
689692
return BuildError{
690693
http.StatusNotAcceptable,
691-
fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus),
692-
xerrors.New(""),
694+
msg,
695+
xerrors.New(msg),
693696
}
694697
case codersdk.ProvisionerJobFailed:
698+
msg := fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String)
695699
return BuildError{
696700
http.StatusBadRequest,
697-
fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String),
698-
xerrors.New(""),
701+
msg,
702+
xerrors.New(msg),
699703
}
700704
case codersdk.ProvisionerJobCanceled:
705+
msg := fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String)
701706
return BuildError{
702707
http.StatusBadRequest,
703-
"The provided template version was canceled during import. You cannot build workspaces with it!",
704-
xerrors.New(""),
708+
msg,
709+
xerrors.New(msg),
705710
}
706711
}
707712
return nil
@@ -717,10 +722,11 @@ func (b *Builder) checkRunningBuild() error {
717722
return BuildError{http.StatusInternalServerError, "failed to fetch prior build", err}
718723
}
719724
if db2sdk.ProvisionerJobStatus(*job).Active() {
725+
msg := "A workspace build is already active."
720726
return BuildError{
721727
http.StatusConflict,
722-
"A workspace build is already active.",
723-
xerrors.New(""),
728+
msg,
729+
xerrors.New(msg),
724730
}
725731
}
726732
return nil

0 commit comments

Comments
 (0)