Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 3 additions & 3 deletions enterprise/coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -1104,9 +1104,9 @@ func (api *API) CheckBuildUsage(
task *database.Task,
transition database.WorkspaceTransition,
) (wsbuilder.UsageCheckResponse, error) {
// If the template version has an external agent, we need to check that the
// license is entitled to this feature.
if templateVersion.HasExternalAgent.Valid && templateVersion.HasExternalAgent.Bool {
// External-agent templates require an entitlement for start builds.
if transition == database.WorkspaceTransitionStart &&
templateVersion.HasExternalAgent.Valid && templateVersion.HasExternalAgent.Bool {
feature, ok := api.Entitlements.Feature(codersdk.FeatureWorkspaceExternalAgent)
if !ok || !feature.Enabled {
return wsbuilder.UsageCheckResponse{
Expand Down
73 changes: 50 additions & 23 deletions enterprise/coderd/coderd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -942,37 +942,68 @@ func TestCheckBuildUsage_NeverBlocksOnManagedAgentLimit(t *testing.T) {
require.True(t, deleteResp.Permitted)
}

func TestCheckBuildUsage_BlocksWithoutManagedAgentEntitlement(t *testing.T) {
func TestCheckBuildUsage_BlocksStartWithoutEntitlement(t *testing.T) {
t.Parallel()

tv := &database.TemplateVersion{
managedAgentVersion := &database.TemplateVersion{
HasAITask: sql.NullBool{Valid: true, Bool: true},
HasExternalAgent: sql.NullBool{Valid: true, Bool: false},
}
task := &database.Task{
TemplateVersionID: tv.ID,
externalAgentVersion := &database.TemplateVersion{
HasExternalAgent: sql.NullBool{Valid: true, Bool: true},
}

// Both "feature absent" and "feature explicitly disabled" should
// block AI task builds on licensed deployments.
tests := []struct {
name string
setupEnts func(e *codersdk.Entitlements)
name string
templateVersion *database.TemplateVersion
task *database.Task
setupEnts func(e *codersdk.Entitlements)
message string
checkNoTaskStart bool
}{
{
name: "FeatureAbsent",
name: "ManagedAgentFeatureAbsent",
templateVersion: managedAgentVersion,
task: &database.Task{TemplateVersionID: managedAgentVersion.ID},
setupEnts: func(e *codersdk.Entitlements) {
e.HasLicense = true
delete(e.Features, codersdk.FeatureManagedAgentLimit)
},
message: "not entitled to managed agents",
checkNoTaskStart: true,
},
{
name: "FeatureDisabled",
name: "ManagedAgentFeatureDisabled",
templateVersion: managedAgentVersion,
task: &database.Task{TemplateVersionID: managedAgentVersion.ID},
setupEnts: func(e *codersdk.Entitlements) {
e.HasLicense = true
e.Features[codersdk.FeatureManagedAgentLimit] = codersdk.Feature{
Enabled: false,
}
},
message: "not entitled to managed agents",
checkNoTaskStart: true,
},
{
name: "ExternalAgentFeatureAbsent",
templateVersion: externalAgentVersion,
setupEnts: func(e *codersdk.Entitlements) {
e.HasLicense = true
delete(e.Features, codersdk.FeatureWorkspaceExternalAgent)
},
message: "uses external agents",
},
{
name: "ExternalAgentFeatureDisabled",
templateVersion: externalAgentVersion,
setupEnts: func(e *codersdk.Entitlements) {
e.HasLicense = true
e.Features[codersdk.FeatureWorkspaceExternalAgent] = codersdk.Feature{
Enabled: false,
}
},
message: "uses external agents",
},
}

Expand All @@ -998,28 +1029,24 @@ func TestCheckBuildUsage_BlocksWithoutManagedAgentEntitlement(t *testing.T) {
mDB := dbmock.NewMockStore(ctrl)
ctx := context.Background()

// Start transition with a task: should be blocked because the
// license doesn't include the managed agent entitlement.
resp, err := eapi.CheckBuildUsage(ctx, mDB, tv, task, database.WorkspaceTransitionStart)
resp, err := eapi.CheckBuildUsage(ctx, mDB, tc.templateVersion, tc.task, database.WorkspaceTransitionStart)
require.NoError(t, err)
require.False(t, resp.Permitted)
require.Contains(t, resp.Message, "not entitled to managed agents")
require.Contains(t, resp.Message, tc.message)

// Stop and delete transitions should still be permitted so
// that existing workspaces can be stopped/cleaned up.
stopResp, err := eapi.CheckBuildUsage(ctx, mDB, tv, task, database.WorkspaceTransitionStop)
stopResp, err := eapi.CheckBuildUsage(ctx, mDB, tc.templateVersion, tc.task, database.WorkspaceTransitionStop)
require.NoError(t, err)
require.True(t, stopResp.Permitted)

deleteResp, err := eapi.CheckBuildUsage(ctx, mDB, tv, task, database.WorkspaceTransitionDelete)
deleteResp, err := eapi.CheckBuildUsage(ctx, mDB, tc.templateVersion, tc.task, database.WorkspaceTransitionDelete)
require.NoError(t, err)
require.True(t, deleteResp.Permitted)

// Start transition without a task: should be permitted (not
// an AI task build, so the entitlement check doesn't apply).
noTaskResp, err := eapi.CheckBuildUsage(ctx, mDB, tv, nil, database.WorkspaceTransitionStart)
require.NoError(t, err)
require.True(t, noTaskResp.Permitted)
if tc.checkNoTaskStart {
noTaskResp, err := eapi.CheckBuildUsage(ctx, mDB, tc.templateVersion, nil, database.WorkspaceTransitionStart)
require.NoError(t, err)
require.True(t, noTaskResp.Permitted)
}
})
}
}
Expand Down
Loading