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:
Ammar Bandukwala
2023-08-21 21:55:39 -05:00
committed by GitHub
parent 69ec8d774b
commit 545a256b57
7 changed files with 293 additions and 55 deletions

View File

@ -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)
})
}