Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Enable automatic updates on autostart
Signed-off-by: Spike Curtis <spike@coder.com>
  • Loading branch information
spikecurtis committed Oct 6, 2023
commit 42008be51cf1c1481437105f60e61a307609fa9d
6 changes: 6 additions & 0 deletions coderd/autobuild/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ func (e *Executor) runOnce(t time.Time) Stats {
SetLastWorkspaceBuildInTx(&latestBuild).
SetLastWorkspaceBuildJobInTx(&latestJob).
Reason(reason)
log.Debug(e.ctx, "auto building workspace", slog.F("transition", nextTransition))
if nextTransition == database.WorkspaceTransitionStart &&
ws.AutomaticUpdates == database.AutomaticUpdatesAlways {
log.Debug(e.ctx, "autostarting with active version")
builder = builder.ActiveVersion()
}

_, job, err = builder.Build(e.ctx, tx, nil)
if err != nil {
Expand Down
166 changes: 123 additions & 43 deletions coderd/autobuild/lifecycle_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"testing"
"time"

"cdr.dev/slog"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -64,50 +66,128 @@ func TestExecutorAutostartOK(t *testing.T) {
func TestExecutorAutostartTemplateUpdated(t *testing.T) {
t.Parallel()

var (
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
ctx = context.Background()
err error
tickCh = make(chan time.Time)
statsCh = make(chan autobuild.Stats)
client = coderdtest.New(t, &coderdtest.Options{
AutobuildTicker: tickCh,
IncludeProvisionerDaemon: true,
AutobuildStats: statsCh,
})
// Given: we have a user with a workspace that has autostart enabled
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.AutostartSchedule = ptr.Ref(sched.String())
})
)
// Given: workspace is stopped
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)

// Given: the workspace template has been updated
orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID.String())
require.NoError(t, err)
require.Len(t, orgs, 1)

newVersion := coderdtest.UpdateTemplateVersion(t, client, orgs[0].ID, nil, workspace.TemplateID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, newVersion.ID)
require.NoError(t, client.UpdateActiveTemplateVersion(ctx, workspace.TemplateID, codersdk.UpdateActiveTemplateVersion{
ID: newVersion.ID,
}))

// When: the autobuild executor ticks after the scheduled time
go func() {
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
close(tickCh)
}()
testCases := []struct {
name string
automaticUpdates codersdk.AutomaticUpdates
compatibleParameters bool
expectStart bool
expectUpdate bool
}{
{
name: "Never",
automaticUpdates: codersdk.AutomaticUpdatesNever,
compatibleParameters: true,
expectStart: true,
expectUpdate: false,
},
{
name: "Always_Compatible",
automaticUpdates: codersdk.AutomaticUpdatesAlways,
compatibleParameters: true,
expectStart: true,
expectUpdate: true,
},
{
name: "Always_Incompatible",
automaticUpdates: codersdk.AutomaticUpdatesAlways,
compatibleParameters: false,
expectStart: false,
expectUpdate: false,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var (
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
ctx = context.Background()
err error
tickCh = make(chan time.Time)
statsCh = make(chan autobuild.Stats)
logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: !tc.expectStart}).Leveled(slog.LevelDebug)
client = coderdtest.New(t, &coderdtest.Options{
AutobuildTicker: tickCh,
IncludeProvisionerDaemon: true,
AutobuildStats: statsCh,
Logger: &logger,
})
// Given: we have a user with a workspace that has autostart enabled
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.AutostartSchedule = ptr.Ref(sched.String())
// Given: automatic updates from the test case
cwr.AutomaticUpdates = tc.automaticUpdates
})
)
// Given: workspace is stopped
workspace = coderdtest.MustTransitionWorkspace(
t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)

// Given: the workspace template has been updated
orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID.String())
require.NoError(t, err)
require.Len(t, orgs, 1)

var res *echo.Responses
if !tc.compatibleParameters {
// Given, parameters of the new version are not compatible.
// Since initial version has no parameters, any parameters in the new version will be incompatible
res = &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Parameters: []*proto.RichParameter{
{
Name: "new",
Mutable: false,
Required: true,
},
},
},
},
}},
}
}

// Then: the workspace should be started using the previous template version, and not the updated version.
stats := <-statsCh
assert.NoError(t, stats.Error)
assert.Len(t, stats.Transitions, 1)
assert.Contains(t, stats.Transitions, workspace.ID)
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
ws := coderdtest.MustWorkspace(t, client, workspace.ID)
assert.Equal(t, workspace.LatestBuild.TemplateVersionID, ws.LatestBuild.TemplateVersionID, "expected workspace build to be using the old template version")
newVersion := coderdtest.UpdateTemplateVersion(t, client, orgs[0].ID, res, workspace.TemplateID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, newVersion.ID)
require.NoError(t, client.UpdateActiveTemplateVersion(
ctx, workspace.TemplateID, codersdk.UpdateActiveTemplateVersion{
ID: newVersion.ID,
},
))

t.Log("sending autobuild tick")
// When: the autobuild executor ticks after the scheduled time
go func() {
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
close(tickCh)
}()

stats := <-statsCh
assert.NoError(t, stats.Error)
if tc.expectStart {
// Then: the workspace should be started
assert.Len(t, stats.Transitions, 1)
assert.Contains(t, stats.Transitions, workspace.ID)
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
ws := coderdtest.MustWorkspace(t, client, workspace.ID)
if tc.expectUpdate {
// Then: uses the updated version
assert.Equal(t, newVersion.ID, ws.LatestBuild.TemplateVersionID,
"expected workspace build to be using the updated template version")
} else {
// Then: uses the previous template version
assert.Equal(t, workspace.LatestBuild.TemplateVersionID, ws.LatestBuild.TemplateVersionID,
"expected workspace build to be using the old template version")
}
} else {
// Then: the workspace should not be started
assert.Len(t, stats.Transitions, 0)
}
})
}
}

func TestExecutorAutostartAlreadyRunning(t *testing.T) {
Expand Down
16 changes: 8 additions & 8 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
if options == nil {
options = &Options{}
}
if options.Logger == nil {
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
options.Logger = &logger
}
if options.GoogleTokenValidator == nil {
ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(cancelFunc)
Expand Down Expand Up @@ -214,7 +218,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can

if options.Database == nil {
options.Database, options.Pubsub = dbtestutil.NewDB(t)
options.Database = dbauthz.New(options.Database, options.Authorizer, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
options.Database = dbauthz.New(options.Database, options.Authorizer, options.Logger.Leveled(slog.LevelDebug))
}

// Some routes expect a deployment ID, so just make sure one exists.
Expand Down Expand Up @@ -268,14 +272,14 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
options.Database,
options.Pubsub,
&templateScheduleStore,
slogtest.Make(t, nil).Named("autobuild.executor").Leveled(slog.LevelDebug),
*options.Logger,
options.AutobuildTicker,
).WithStatsChannel(options.AutobuildStats)
lifecycleExecutor.Run()

hangDetectorTicker := time.NewTicker(options.DeploymentValues.JobHangDetectorInterval.Value())
defer hangDetectorTicker.Stop()
hangDetector := unhanger.New(ctx, options.Database, options.Pubsub, slogtest.Make(t, nil).Named("unhanger.detector"), hangDetectorTicker.C)
hangDetector := unhanger.New(ctx, options.Database, options.Pubsub, options.Logger.Named("unhanger.detector"), hangDetectorTicker.C)
hangDetector.Start()
t.Cleanup(hangDetector.Close)

Expand Down Expand Up @@ -334,7 +338,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
stunAddresses = options.DeploymentValues.DERP.Server.STUNAddresses.Value()
}

derpServer := derp.NewServer(key.NewNode(), tailnet.Logger(slogtest.Make(t, nil).Named("derp").Leveled(slog.LevelDebug)))
derpServer := derp.NewServer(key.NewNode(), tailnet.Logger(options.Logger.Named("derp").Leveled(slog.LevelDebug)))
derpServer.SetMeshKey("test-key")

// match default with cli default
Expand All @@ -349,10 +353,6 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
require.NoError(t, err)
}

if options.Logger == nil {
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
options.Logger = &logger
}
region := &tailcfg.DERPRegion{
EmbeddedRelay: true,
RegionID: int(options.DeploymentValues.DERP.Server.RegionID.Value()),
Expand Down
2 changes: 1 addition & 1 deletion coderd/database/db2sdk/db2sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func TestProvisionerJobStatus(t *testing.T) {
// Make sure the inserted job has the right values.
require.Equal(t, tc.job.StartedAt.Time.UTC(), inserted.StartedAt.Time.UTC(), "started at")
require.Equal(t, tc.job.CompletedAt.Time.UTC(), inserted.CompletedAt.Time.UTC(), "completed at")
require.Equal(t, tc.job.CanceledAt.Time.UTC(), inserted.CanceledAt.Time.UTC(), "cancelled at")
require.Equal(t, tc.job.CanceledAt.Time.UTC(), inserted.CanceledAt.Time.UTC(), "canceled at")
require.Equal(t, tc.job.Error, inserted.Error, "error")
require.Equal(t, tc.job.ErrorCode, inserted.ErrorCode, "error code")

Expand Down