mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
fix: correctly reject quota-violating builds (#9233)
Due to a logical error in CommitQuota, all workspace Stop->Start operations were being accepted, regardless of the Quota limit. This issue only appeared after #9201, so this was a minor regression in main for about 3 days. This PR adds a test to make sure this kind of bug doesn't recur. To make the new test possible, we give the echo provisioner the ability to simulate responses to specific transitions.
This commit is contained in:
@ -10,6 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||
@ -31,12 +32,13 @@ func verifyQuota(ctx context.Context, t *testing.T, client *codersdk.Client, con
|
||||
}
|
||||
|
||||
func TestWorkspaceQuota(t *testing.T) {
|
||||
// TODO: refactor for new impl
|
||||
|
||||
t.Parallel()
|
||||
|
||||
t.Run("BlocksBuild", func(t *testing.T) {
|
||||
// This first test verifies the behavior of creating and deleting workspaces.
|
||||
// It also tests multi-group quota stacking and the everyone group.
|
||||
t.Run("CreateDelete", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
max := 1
|
||||
@ -49,8 +51,6 @@ func TestWorkspaceQuota(t *testing.T) {
|
||||
},
|
||||
})
|
||||
coderdtest.NewProvisionerDaemon(t, api.AGPL)
|
||||
coderdtest.NewProvisionerDaemon(t, api.AGPL)
|
||||
coderdtest.NewProvisionerDaemon(t, api.AGPL)
|
||||
|
||||
verifyQuota(ctx, t, client, 0, 0)
|
||||
|
||||
@ -157,4 +157,104 @@ func TestWorkspaceQuota(t *testing.T) {
|
||||
verifyQuota(ctx, t, client, 4, 4)
|
||||
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||||
})
|
||||
|
||||
t.Run("StartStop", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
max := 1
|
||||
client, _, api, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
|
||||
UserWorkspaceQuota: max,
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
coderdtest.NewProvisionerDaemon(t, api.AGPL)
|
||||
|
||||
verifyQuota(ctx, t, client, 0, 0)
|
||||
|
||||
// Patch the 'Everyone' group to verify its quota allowance is being accounted for.
|
||||
_, err := client.PatchGroup(ctx, user.OrganizationID, codersdk.PatchGroupRequest{
|
||||
QuotaAllowance: ptr.Ref(4),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyQuota(ctx, t, client, 0, 4)
|
||||
|
||||
stopResp := []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Complete{
|
||||
Complete: &proto.Provision_Complete{
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "example",
|
||||
Type: "aws_instance",
|
||||
DailyCost: 1,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
startResp := []*proto.Provision_Response{{
|
||||
Type: &proto.Provision_Response_Complete{
|
||||
Complete: &proto.Provision_Complete{
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "example",
|
||||
Type: "aws_instance",
|
||||
DailyCost: 2,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlanMap: map[proto.WorkspaceTransition][]*proto.Provision_Response{
|
||||
proto.WorkspaceTransition_START: startResp,
|
||||
proto.WorkspaceTransition_STOP: stopResp,
|
||||
},
|
||||
ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Provision_Response{
|
||||
proto.WorkspaceTransition_START: startResp,
|
||||
proto.WorkspaceTransition_STOP: stopResp,
|
||||
},
|
||||
})
|
||||
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
|
||||
// Spin up two workspaces.
|
||||
var wg sync.WaitGroup
|
||||
var workspaces []codersdk.Workspace
|
||||
for i := 0; i < 2; i++ {
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
workspaces = append(workspaces, workspace)
|
||||
build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
assert.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||||
}
|
||||
wg.Wait()
|
||||
verifyQuota(ctx, t, client, 4, 4)
|
||||
|
||||
// Next one must fail
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
require.Contains(t, build.Job.Error, "quota")
|
||||
|
||||
// Consumed shouldn't bump
|
||||
verifyQuota(ctx, t, client, 4, 4)
|
||||
require.Equal(t, codersdk.WorkspaceStatusFailed, build.Status)
|
||||
|
||||
build = coderdtest.CreateWorkspaceBuild(t, client, workspaces[0], database.WorkspaceTransitionStop)
|
||||
build = coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
|
||||
// Quota goes down one
|
||||
verifyQuota(ctx, t, client, 3, 4)
|
||||
require.Equal(t, codersdk.WorkspaceStatusStopped, build.Status)
|
||||
|
||||
build = coderdtest.CreateWorkspaceBuild(t, client, workspaces[0], database.WorkspaceTransitionStart)
|
||||
build = coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
|
||||
|
||||
// Quota goes back up
|
||||
verifyQuota(ctx, t, client, 4, 4)
|
||||
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user