Skip to content

Commit 4502851

Browse files
committed
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy