Skip to content

Commit c03ea52

Browse files
committed
add tests to ensure workspace builds that include a preset have it set correctly
1 parent 82e016c commit c03ea52

File tree

6 files changed

+433
-0
lines changed

6 files changed

+433
-0
lines changed

coderd/provisionerdserver/provisionerdserver_test.go

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,280 @@ func TestAcquireJob(t *testing.T) {
436436
_, err = db.GetAPIKeyByID(ctx, key.ID)
437437
require.ErrorIs(t, err, sql.ErrNoRows)
438438
})
439+
t.Run(tc.name+"_PrebuiltWorkspaceBuildJob", func(t *testing.T) {
440+
t.Parallel()
441+
// Set the max session token lifetime so we can assert we
442+
// create an API key with an expiration within the bounds of the
443+
// deployment config.
444+
dv := &codersdk.DeploymentValues{
445+
Sessions: codersdk.SessionLifetime{
446+
MaximumTokenDuration: serpent.Duration(time.Hour),
447+
},
448+
}
449+
gitAuthProvider := &sdkproto.ExternalAuthProviderResource{
450+
Id: "github",
451+
}
452+
453+
srv, db, ps, pd := setup(t, false, &overrides{
454+
deploymentValues: dv,
455+
externalAuthConfigs: []*externalauth.Config{{
456+
ID: gitAuthProvider.Id,
457+
InstrumentedOAuth2Config: &testutil.OAuth2Config{},
458+
}},
459+
})
460+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
461+
defer cancel()
462+
463+
user := dbgen.User(t, db, database.User{})
464+
group1 := dbgen.Group(t, db, database.Group{
465+
Name: "group1",
466+
OrganizationID: pd.OrganizationID,
467+
})
468+
sshKey := dbgen.GitSSHKey(t, db, database.GitSSHKey{
469+
UserID: user.ID,
470+
})
471+
err := db.InsertGroupMember(ctx, database.InsertGroupMemberParams{
472+
UserID: user.ID,
473+
GroupID: group1.ID,
474+
})
475+
require.NoError(t, err)
476+
link := dbgen.UserLink(t, db, database.UserLink{
477+
LoginType: database.LoginTypeOIDC,
478+
UserID: user.ID,
479+
OAuthExpiry: dbtime.Now().Add(time.Hour),
480+
OAuthAccessToken: "access-token",
481+
})
482+
dbgen.ExternalAuthLink(t, db, database.ExternalAuthLink{
483+
ProviderID: gitAuthProvider.Id,
484+
UserID: user.ID,
485+
})
486+
template := dbgen.Template(t, db, database.Template{
487+
Name: "template",
488+
Provisioner: database.ProvisionerTypeEcho,
489+
OrganizationID: pd.OrganizationID,
490+
})
491+
file := dbgen.File(t, db, database.File{CreatedBy: user.ID})
492+
versionFile := dbgen.File(t, db, database.File{CreatedBy: user.ID})
493+
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
494+
OrganizationID: pd.OrganizationID,
495+
TemplateID: uuid.NullUUID{
496+
UUID: template.ID,
497+
Valid: true,
498+
},
499+
JobID: uuid.New(),
500+
})
501+
externalAuthProviders, err := json.Marshal([]database.ExternalAuthProvider{{
502+
ID: gitAuthProvider.Id,
503+
Optional: gitAuthProvider.Optional,
504+
}})
505+
require.NoError(t, err)
506+
err = db.UpdateTemplateVersionExternalAuthProvidersByJobID(ctx, database.UpdateTemplateVersionExternalAuthProvidersByJobIDParams{
507+
JobID: version.JobID,
508+
ExternalAuthProviders: json.RawMessage(externalAuthProviders),
509+
UpdatedAt: dbtime.Now(),
510+
})
511+
require.NoError(t, err)
512+
// Import version job
513+
_ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{
514+
OrganizationID: pd.OrganizationID,
515+
ID: version.JobID,
516+
InitiatorID: user.ID,
517+
FileID: versionFile.ID,
518+
Provisioner: database.ProvisionerTypeEcho,
519+
StorageMethod: database.ProvisionerStorageMethodFile,
520+
Type: database.ProvisionerJobTypeTemplateVersionImport,
521+
Input: must(json.Marshal(provisionerdserver.TemplateVersionImportJob{
522+
TemplateVersionID: version.ID,
523+
UserVariableValues: []codersdk.VariableValue{
524+
{Name: "second", Value: "bah"},
525+
},
526+
})),
527+
})
528+
_ = dbgen.TemplateVersionVariable(t, db, database.TemplateVersionVariable{
529+
TemplateVersionID: version.ID,
530+
Name: "first",
531+
Value: "first_value",
532+
DefaultValue: "default_value",
533+
Sensitive: true,
534+
})
535+
_ = dbgen.TemplateVersionVariable(t, db, database.TemplateVersionVariable{
536+
TemplateVersionID: version.ID,
537+
Name: "second",
538+
Value: "second_value",
539+
DefaultValue: "default_value",
540+
Required: true,
541+
Sensitive: false,
542+
})
543+
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
544+
TemplateID: template.ID,
545+
OwnerID: user.ID,
546+
OrganizationID: pd.OrganizationID,
547+
})
548+
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
549+
WorkspaceID: workspace.ID,
550+
BuildNumber: 1,
551+
JobID: uuid.New(),
552+
TemplateVersionID: version.ID,
553+
Transition: database.WorkspaceTransitionStart,
554+
Reason: database.BuildReasonInitiator,
555+
})
556+
_ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{
557+
ID: build.ID,
558+
OrganizationID: pd.OrganizationID,
559+
InitiatorID: user.ID,
560+
Provisioner: database.ProvisionerTypeEcho,
561+
StorageMethod: database.ProvisionerStorageMethodFile,
562+
FileID: file.ID,
563+
Type: database.ProvisionerJobTypeWorkspaceBuild,
564+
Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{
565+
WorkspaceBuildID: build.ID,
566+
IsPrebuild: true,
567+
})),
568+
})
569+
570+
startPublished := make(chan struct{})
571+
var closed bool
572+
closeStartSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID),
573+
wspubsub.HandleWorkspaceEvent(
574+
func(_ context.Context, e wspubsub.WorkspaceEvent, err error) {
575+
if err != nil {
576+
return
577+
}
578+
if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID {
579+
if !closed {
580+
close(startPublished)
581+
closed = true
582+
}
583+
}
584+
}))
585+
require.NoError(t, err)
586+
defer closeStartSubscribe()
587+
588+
var job *proto.AcquiredJob
589+
590+
for {
591+
// Grab jobs until we find the workspace build job. There is also
592+
// an import version job that we need to ignore.
593+
job, err = tc.acquire(ctx, srv)
594+
require.NoError(t, err)
595+
if _, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_); ok {
596+
break
597+
}
598+
}
599+
600+
<-startPublished
601+
602+
got, err := json.Marshal(job.Type)
603+
require.NoError(t, err)
604+
605+
// Validate that a session token is generated during the job.
606+
sessionToken := job.Type.(*proto.AcquiredJob_WorkspaceBuild_).WorkspaceBuild.Metadata.WorkspaceOwnerSessionToken
607+
require.NotEmpty(t, sessionToken)
608+
toks := strings.Split(sessionToken, "-")
609+
require.Len(t, toks, 2, "invalid api key")
610+
key, err := db.GetAPIKeyByID(ctx, toks[0])
611+
require.NoError(t, err)
612+
require.Equal(t, int64(dv.Sessions.MaximumTokenDuration.Value().Seconds()), key.LifetimeSeconds)
613+
require.WithinDuration(t, time.Now().Add(dv.Sessions.MaximumTokenDuration.Value()), key.ExpiresAt, time.Minute)
614+
615+
want, err := json.Marshal(&proto.AcquiredJob_WorkspaceBuild_{
616+
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
617+
WorkspaceBuildId: build.ID.String(),
618+
WorkspaceName: workspace.Name,
619+
VariableValues: []*sdkproto.VariableValue{
620+
{
621+
Name: "first",
622+
Value: "first_value",
623+
Sensitive: true,
624+
},
625+
{
626+
Name: "second",
627+
Value: "second_value",
628+
},
629+
},
630+
ExternalAuthProviders: []*sdkproto.ExternalAuthProvider{{
631+
Id: gitAuthProvider.Id,
632+
AccessToken: "access_token",
633+
}},
634+
Metadata: &sdkproto.Metadata{
635+
CoderUrl: (&url.URL{}).String(),
636+
WorkspaceTransition: sdkproto.WorkspaceTransition_START,
637+
WorkspaceName: workspace.Name,
638+
WorkspaceOwner: user.Username,
639+
WorkspaceOwnerEmail: user.Email,
640+
WorkspaceOwnerName: user.Name,
641+
WorkspaceOwnerOidcAccessToken: link.OAuthAccessToken,
642+
WorkspaceOwnerGroups: []string{group1.Name},
643+
WorkspaceId: workspace.ID.String(),
644+
WorkspaceOwnerId: user.ID.String(),
645+
TemplateId: template.ID.String(),
646+
TemplateName: template.Name,
647+
TemplateVersion: version.Name,
648+
WorkspaceOwnerSessionToken: sessionToken,
649+
WorkspaceOwnerSshPublicKey: sshKey.PublicKey,
650+
WorkspaceOwnerSshPrivateKey: sshKey.PrivateKey,
651+
WorkspaceBuildId: build.ID.String(),
652+
WorkspaceOwnerLoginType: string(user.LoginType),
653+
WorkspaceOwnerRbacRoles: []*sdkproto.Role{{Name: "member", OrgId: pd.OrganizationID.String()}},
654+
IsPrebuild: true,
655+
},
656+
},
657+
})
658+
require.NoError(t, err)
659+
660+
require.JSONEq(t, string(want), string(got))
661+
662+
// Assert that we delete the session token whenever
663+
// a stop is issued.
664+
stopbuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
665+
WorkspaceID: workspace.ID,
666+
BuildNumber: 2,
667+
JobID: uuid.New(),
668+
TemplateVersionID: version.ID,
669+
Transition: database.WorkspaceTransitionStop,
670+
Reason: database.BuildReasonInitiator,
671+
})
672+
_ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{
673+
ID: stopbuild.ID,
674+
InitiatorID: user.ID,
675+
Provisioner: database.ProvisionerTypeEcho,
676+
StorageMethod: database.ProvisionerStorageMethodFile,
677+
FileID: file.ID,
678+
Type: database.ProvisionerJobTypeWorkspaceBuild,
679+
Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{
680+
WorkspaceBuildID: stopbuild.ID,
681+
})),
682+
})
683+
684+
stopPublished := make(chan struct{})
685+
closeStopSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID),
686+
wspubsub.HandleWorkspaceEvent(
687+
func(_ context.Context, e wspubsub.WorkspaceEvent, err error) {
688+
if err != nil {
689+
return
690+
}
691+
if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID {
692+
close(stopPublished)
693+
}
694+
}))
695+
require.NoError(t, err)
696+
defer closeStopSubscribe()
697+
698+
// Grab jobs until we find the workspace build job. There is also
699+
// an import version job that we need to ignore.
700+
job, err = tc.acquire(ctx, srv)
701+
require.NoError(t, err)
702+
_, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_)
703+
require.True(t, ok, "acquired job not a workspace build?")
704+
705+
<-stopPublished
706+
707+
// Validate that a session token is deleted during a stop job.
708+
sessionToken = job.Type.(*proto.AcquiredJob_WorkspaceBuild_).WorkspaceBuild.Metadata.WorkspaceOwnerSessionToken
709+
require.Empty(t, sessionToken)
710+
_, err = db.GetAPIKeyByID(ctx, key.ID)
711+
require.ErrorIs(t, err, sql.ErrNoRows)
712+
})
439713

440714
t.Run(tc.name+"_TemplateVersionDryRun", func(t *testing.T) {
441715
t.Parallel()

coderd/workspacebuilds.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,11 @@ func (api *API) convertWorkspaceBuild(
10661066
return apiResources[i].Name < apiResources[j].Name
10671067
})
10681068

1069+
var presetID *uuid.UUID
1070+
if build.TemplateVersionPresetID.Valid {
1071+
presetID = &build.TemplateVersionPresetID.UUID
1072+
}
1073+
10691074
apiJob := convertProvisionerJob(job)
10701075
transition := codersdk.WorkspaceTransition(build.Transition)
10711076
return codersdk.WorkspaceBuild{
@@ -1091,6 +1096,7 @@ func (api *API) convertWorkspaceBuild(
10911096
Status: codersdk.ConvertWorkspaceStatus(apiJob.Status, transition),
10921097
DailyCost: build.DailyCost,
10931098
MatchedProvisioners: &matchedProvisioners,
1099+
TemplateVersionPresetID: presetID,
10941100
}, nil
10951101
}
10961102

coderd/workspacebuilds_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,50 @@ func TestPostWorkspaceBuild(t *testing.T) {
13071307
require.Equal(t, wantState, gotState)
13081308
})
13091309

1310+
t.Run("SetsPresetID", func(t *testing.T) {
1311+
t.Parallel()
1312+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
1313+
user := coderdtest.CreateFirstUser(t, client)
1314+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
1315+
Parse: echo.ParseComplete,
1316+
ProvisionPlan: []*proto.Response{{
1317+
Type: &proto.Response_Plan{
1318+
Plan: &proto.PlanComplete{
1319+
Presets: []*proto.Preset{{
1320+
Name: "test",
1321+
}},
1322+
},
1323+
},
1324+
}},
1325+
ProvisionApply: echo.ApplyComplete,
1326+
})
1327+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
1328+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
1329+
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
1330+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
1331+
require.Nil(t, workspace.LatestBuild.TemplateVersionPresetID)
1332+
1333+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
1334+
defer cancel()
1335+
1336+
presets, err := client.TemplateVersionPresets(ctx, version.ID)
1337+
require.NoError(t, err)
1338+
require.Equal(t, 1, len(presets))
1339+
require.Equal(t, "test", presets[0].Name)
1340+
1341+
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
1342+
TemplateVersionID: version.ID,
1343+
Transition: codersdk.WorkspaceTransitionStart,
1344+
TemplateVersionPresetID: presets[0].ID,
1345+
})
1346+
require.NoError(t, err)
1347+
require.NotNil(t, build.TemplateVersionPresetID)
1348+
1349+
workspace, err = client.Workspace(ctx, workspace.ID)
1350+
require.NoError(t, err)
1351+
require.Equal(t, build.TemplateVersionPresetID, workspace.LatestBuild.TemplateVersionPresetID)
1352+
})
1353+
13101354
t.Run("Delete", func(t *testing.T) {
13111355
t.Parallel()
13121356
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})

0 commit comments

Comments
 (0)