@@ -2,17 +2,30 @@ package cli_test
22
33import (
44"bytes"
5+ "database/sql"
56"net/http"
67"testing"
8+ "time"
79
10+ "github.com/google/uuid"
811"github.com/stretchr/testify/assert"
912"github.com/stretchr/testify/require"
1013
1114"github.com/coder/coder/v2/cli/clitest"
1215"github.com/coder/coder/v2/coderd/coderdtest"
16+ "github.com/coder/coder/v2/coderd/database"
17+ "github.com/coder/coder/v2/coderd/database/dbauthz"
18+ "github.com/coder/coder/v2/coderd/database/dbfake"
19+ "github.com/coder/coder/v2/coderd/database/dbgen"
20+ "github.com/coder/coder/v2/coderd/database/dbtime"
21+ "github.com/coder/coder/v2/coderd/util/ptr"
1322"github.com/coder/coder/v2/codersdk"
1423"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
1524"github.com/coder/coder/v2/enterprise/coderd/license"
25+ "github.com/coder/coder/v2/provisionersdk/proto"
26+ "github.com/coder/coder/v2/pty/ptytest"
27+ "github.com/coder/coder/v2/testutil"
28+ "github.com/coder/quartz"
1629)
1730
1831func TestPrebuildsPause (t * testing.T ) {
@@ -341,3 +354,139 @@ func TestPrebuildsSettingsAPI(t *testing.T) {
341354assert .False (t ,settings .ReconciliationPaused )
342355})
343356}
357+
358+ // TestSchedulePrebuilds verifies the CLI schedule command when used with prebuilds.
359+ // Running the command on an unclaimed prebuild fails, but after the prebuild is
360+ // claimed (becoming a regular workspace) it succeeds as expected.
361+ func TestSchedulePrebuilds (t * testing.T ) {
362+ t .Parallel ()
363+
364+ cases := []struct {
365+ name string
366+ cliErrorMsg string
367+ cmdArgs func (string ) []string
368+ }{
369+ {
370+ name :"AutostartPrebuildError" ,
371+ cliErrorMsg :"autostart configuration is not supported for prebuilt workspaces" ,
372+ cmdArgs :func (workspaceName string ) []string {
373+ return []string {"schedule" ,"start" ,workspaceName ,"7:30AM" ,"Mon-Fri" ,"Europe/Lisbon" }
374+ },
375+ },
376+ {
377+ name :"AutostopPrebuildError" ,
378+ cliErrorMsg :"autostop configuration is not supported for prebuilt workspaces" ,
379+ cmdArgs :func (workspaceName string ) []string {
380+ return []string {"schedule" ,"stop" ,workspaceName ,"8h30m" }
381+ },
382+ },
383+ {
384+ name :"ExtendPrebuildError" ,
385+ cliErrorMsg :"extend configuration is not supported for prebuilt workspaces" ,
386+ cmdArgs :func (workspaceName string ) []string {
387+ return []string {"schedule" ,"extend" ,workspaceName ,"90m" }
388+ },
389+ },
390+ }
391+
392+ for _ ,tc := range cases {
393+ tc := tc
394+ t .Run (tc .name ,func (t * testing.T ) {
395+ t .Parallel ()
396+
397+ clock := quartz .NewMock (t )
398+ clock .Set (dbtime .Now ())
399+
400+ // Setup
401+ client ,db ,owner := coderdenttest .NewWithDatabase (t ,& coderdenttest.Options {
402+ Options :& coderdtest.Options {
403+ IncludeProvisionerDaemon :true ,
404+ Clock :clock ,
405+ },
406+ LicenseOptions :& coderdenttest.LicenseOptions {
407+ Features : license.Features {
408+ codersdk .FeatureWorkspacePrebuilds :1 ,
409+ },
410+ },
411+ })
412+
413+ // Given: a template and a template version with preset and a prebuilt workspace
414+ presetID := uuid .New ()
415+ version := coderdtest .CreateTemplateVersion (t ,client ,owner .OrganizationID ,nil )
416+ _ = coderdtest .AwaitTemplateVersionJobCompleted (t ,client ,version .ID )
417+ template := coderdtest .CreateTemplate (t ,client ,owner .OrganizationID ,version .ID )
418+ dbgen .Preset (t ,db , database.InsertPresetParams {
419+ ID :presetID ,
420+ TemplateVersionID :version .ID ,
421+ DesiredInstances : sql.NullInt32 {Int32 :1 ,Valid :true },
422+ })
423+ workspaceBuild := dbfake .WorkspaceBuild (t ,db , database.WorkspaceTable {
424+ OwnerID :database .PrebuildsSystemUserID ,
425+ TemplateID :template .ID ,
426+ }).Seed (database.WorkspaceBuild {
427+ TemplateVersionID :version .ID ,
428+ TemplateVersionPresetID : uuid.NullUUID {
429+ UUID :presetID ,
430+ Valid :true ,
431+ },
432+ }).WithAgent (func (agent []* proto.Agent ) []* proto.Agent {
433+ return agent
434+ }).Do ()
435+
436+ // Mark the prebuilt workspace's agent as ready so the prebuild can be claimed
437+ // nolint:gocritic
438+ ctx := dbauthz .AsSystemRestricted (testutil .Context (t ,testutil .WaitLong ))
439+ agent ,err := db .GetWorkspaceAgentAndLatestBuildByAuthToken (ctx ,uuid .MustParse (workspaceBuild .AgentToken ))
440+ require .NoError (t ,err )
441+ err = db .UpdateWorkspaceAgentLifecycleStateByID (ctx , database.UpdateWorkspaceAgentLifecycleStateByIDParams {
442+ ID :agent .WorkspaceAgent .ID ,
443+ LifecycleState :database .WorkspaceAgentLifecycleStateReady ,
444+ })
445+ require .NoError (t ,err )
446+
447+ // Given: a prebuilt workspace
448+ prebuild := coderdtest .MustWorkspace (t ,client ,workspaceBuild .Workspace .ID )
449+
450+ // When: running the schedule command over a prebuilt workspace
451+ inv ,root := clitest .New (t ,tc .cmdArgs (prebuild .OwnerName + "/" + prebuild .Name )... )
452+ clitest .SetupConfig (t ,client ,root )
453+ ptytest .New (t ).Attach (inv )
454+ doneChan := make (chan struct {})
455+ var runErr error
456+ go func () {
457+ defer close (doneChan )
458+ runErr = inv .Run ()
459+ }()
460+ <- doneChan
461+
462+ // Then: an error should be returned, with an error message specific to the lifecycle parameter
463+ require .Error (t ,runErr )
464+ require .Contains (t ,runErr .Error (),tc .cliErrorMsg )
465+
466+ // Given: the prebuilt workspace is claimed by a user
467+ user ,err := client .User (ctx ,"testUser" )
468+ require .NoError (t ,err )
469+ claimedWorkspace ,err := client .CreateUserWorkspace (ctx ,user .ID .String (), codersdk.CreateWorkspaceRequest {
470+ TemplateVersionID :version .ID ,
471+ TemplateVersionPresetID :presetID ,
472+ Name :coderdtest .RandomUsername (t ),
473+ // The 'extend' command requires the workspace to have an existing deadline.
474+ // To ensure this, we set the workspace's TTL to 1 hour.
475+ TTLMillis : ptr.Ref [int64 ](time .Hour .Milliseconds ()),
476+ })
477+ require .NoError (t ,err )
478+ coderdtest .AwaitWorkspaceBuildJobCompleted (t ,client ,claimedWorkspace .LatestBuild .ID )
479+ workspace := coderdtest .MustWorkspace (t ,client ,claimedWorkspace .ID )
480+ require .Equal (t ,prebuild .ID ,workspace .ID )
481+
482+ // When: running the schedule command over the claimed workspace
483+ inv ,root = clitest .New (t ,tc .cmdArgs (workspace .OwnerName + "/" + workspace .Name )... )
484+ clitest .SetupConfig (t ,client ,root )
485+ pty := ptytest .New (t ).Attach (inv )
486+ require .NoError (t ,inv .Run ())
487+
488+ // Then: the updated schedule should be shown
489+ pty .ExpectMatch (workspace .OwnerName + "/" + workspace .Name )
490+ })
491+ }
492+ }