@@ -436,6 +436,280 @@ func TestAcquireJob(t *testing.T) {
436
436
_ , err = db .GetAPIKeyByID (ctx , key .ID )
437
437
require .ErrorIs (t , err , sql .ErrNoRows )
438
438
})
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
+ })
439
713
440
714
t .Run (tc .name + "_TemplateVersionDryRun" , func (t * testing.T ) {
441
715
t .Parallel ()
0 commit comments