feat(cli): add --username-infix selection to scaletest notifications… (#28515)

This commit is contained in:
Callum Styan
2026-08-25 12:48:00 -07:00
committed by GitHub
parent f4730aed12
commit c4d356ff71
3 changed files with 129 additions and 15 deletions
+24 -8
View File
@@ -2271,6 +2271,14 @@ func getScaletestWorkspaces(ctx context.Context, client *codersdk.Client, owner,
}
func getScaletestUsers(ctx context.Context, client *codersdk.Client) ([]codersdk.User, error) {
return getScaletestUsersWithPrefix(ctx, client, loadtestutil.ScaleTestPrefix+"-")
}
// getScaletestUsersWithPrefix returns scaletest users whose username starts with
// the given full prefix. The prefix partitions users into disjoint pools (for
// example per load generator) so that concurrent reuse runs select
// non-overlapping users.
func getScaletestUsersWithPrefix(ctx context.Context, client *codersdk.Client, prefix string) ([]codersdk.User, error) {
var (
pageNumber = 0
limit = 100
@@ -2279,7 +2287,7 @@ func getScaletestUsers(ctx context.Context, client *codersdk.Client) ([]codersdk
for {
page, err := client.Users(ctx, codersdk.UsersRequest{
Search: "scaletest-",
Search: prefix,
Pagination: codersdk.Pagination{
Offset: pageNumber * limit,
Limit: limit,
@@ -2294,18 +2302,26 @@ func getScaletestUsers(ctx context.Context, client *codersdk.Client) ([]codersdk
break
}
pageUsers := make([]codersdk.User, 0, len(page.Users))
for _, u := range page.Users {
if loadtestutil.IsScaleTestUser(u.Username, u.Email) {
pageUsers = append(pageUsers, u)
}
}
users = append(users, pageUsers...)
users = append(users, filterScaletestUsersByPrefix(page.Users, prefix)...)
}
return users, nil
}
// filterScaletestUsersByPrefix returns the users whose username starts with
// prefix and that look like scaletest users. It is the in-memory selection
// behind getScaletestUsersWithPrefix. The username prefix guard matters because
// the users search matches the term in several fields, not just the username.
func filterScaletestUsersByPrefix(users []codersdk.User, prefix string) []codersdk.User {
filtered := make([]codersdk.User, 0, len(users))
for _, u := range users {
if strings.HasPrefix(u.Username, prefix) && loadtestutil.IsScaleTestUser(u.Username, u.Email) {
filtered = append(filtered, u)
}
}
return filtered
}
func parseTemplate(ctx context.Context, client *codersdk.Client, organizationIDs []uuid.UUID, template string) (tpl codersdk.Template, err error) {
if id, err := uuid.Parse(template); err == nil && id != uuid.Nil {
tpl, err = client.Template(ctx, id)
+77
View File
@@ -0,0 +1,77 @@
package cli
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/scaletest/loadtestutil"
)
// TestFilterScaletestUsersByPrefix covers the pure user-selection logic behind
// notifications --reuse-users: an infix pool must not pick up users from the
// default pool (isolation), non-scaletest users are ignored, and the username
// guard rejects users that only match the prefix in another field.
func TestFilterScaletestUsersByPrefix(t *testing.T) {
t.Parallel()
users := []codersdk.User{
scaletestUser("scaletest-notif-aaaaaaaa-0", "aaaaaaaa-0@scaletest.local"),
scaletestUser("scaletest-notif-bbbbbbbb-1", "bbbbbbbb-1@scaletest.local"),
// Default pool: a scaletest user that is NOT in the notif pool.
scaletestUser("scaletest-cccccccc-0", "cccccccc-0@scaletest.local"),
// Not a scaletest user at all.
scaletestUser("regular-user", "regular@example.com"),
// Scaletest email but a username that does not start with the prefix; the
// guard must reject it even though a search could surface it.
scaletestUser("admin", "scaletest-notif-dddddddd-9@scaletest.local"),
}
cases := []struct {
name string
prefix string
want []string
}{
{
name: "infix isolates its own pool",
prefix: "scaletest-notif-",
want: []string{
"scaletest-notif-aaaaaaaa-0",
"scaletest-notif-bbbbbbbb-1",
},
},
{
name: "default prefix selects all scaletest users",
prefix: loadtestutil.ScaleTestPrefix + "-",
want: []string{
"scaletest-notif-aaaaaaaa-0",
"scaletest-notif-bbbbbbbb-1",
"scaletest-cccccccc-0",
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := filterScaletestUsersByPrefix(users, tc.prefix)
gotNames := make([]string, 0, len(got))
for _, u := range got {
gotNames = append(gotNames, u.Username)
}
require.ElementsMatch(t, tc.want, gotNames)
})
}
}
func scaletestUser(username, email string) codersdk.User {
return codersdk.User{
ReducedUser: codersdk.ReducedUser{
MinimalUser: codersdk.MinimalUser{Username: username},
Email: email,
},
}
}
+28 -7
View File
@@ -37,6 +37,7 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command {
dialTimeout time.Duration
noCleanup bool
reuseUsers bool
usernameInfix string
smtpAPIURL string
tracingFlags = &scaletestTracingFlags{}
@@ -124,7 +125,7 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command {
// Bound token lifetime to just beyond the run so tokens orphaned by a
// hard kill expire quickly rather than at the deployment default.
tokenLifetime := notificationTimeout + dialTimeout + time.Hour
adminReuse, regularReuse, err = selectNotificationReuseUsers(ctx, client, int(templateAdminCount), int(regularUserCount), tokenLifetime)
adminReuse, regularReuse, err = selectNotificationReuseUsers(ctx, client, usernameInfix, int(templateAdminCount), int(regularUserCount), tokenLifetime)
if err != nil {
return err
}
@@ -327,6 +328,12 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command {
Description: "Reuse existing scaletest users instead of creating new ones. Enough users, including template admins, must already exist (see \"coder exp scaletest create-users\") or the command errors. Run only one user-selecting scaletest command at a time to avoid overlapping selections.",
Value: serpent.BoolOf(&reuseUsers),
},
{
Flag: "username-infix",
Env: "CODER_SCALETEST_NOTIFICATION_USERNAME_INFIX",
Description: "Username infix identifying the user pool to reuse when --reuse-users is set. It must match the --username-infix used by the create-users run that provisioned the pool: for example asdf selects users named scaletest-asdf-<random>-<id> so notifications reuses only its own pool and does not compete with other load generators. Leave empty to select any scaletest- user. Ignored without --reuse-users.",
Value: serpent.StringOf(&usernameInfix),
},
{
Flag: "smtp-api-url",
Env: "CODER_SCALETEST_SMTP_API_URL",
@@ -506,10 +513,20 @@ type notificationReuseUser struct {
sessionToken string
}
// selectNotificationReuseUsers selects existing scaletest users and mints a token
// for each, erroring if the pool lacks enough template admins or regular users.
func selectNotificationReuseUsers(ctx context.Context, client *codersdk.Client, adminCount, regularCount int, tokenLifetime time.Duration) (admins, regulars []notificationReuseUser, err error) {
users, err := getScaletestUsers(ctx, client)
// selectNotificationReuseUsers selects existing scaletest users belonging to the
// pool identified by usernameInfix and mints a token for each, erroring if the
// pool lacks enough template admins or regular users. usernameInfix is inserted
// between the mandatory "scaletest-" root and the rest of the username (empty
// selects any scaletest- user), so it isolates this run from other load
// generators that reuse scaletest users concurrently.
func selectNotificationReuseUsers(ctx context.Context, client *codersdk.Client, usernameInfix string, adminCount, regularCount int, tokenLifetime time.Duration) (admins, regulars []notificationReuseUser, err error) {
// The scaletest- root is always kept so selection matches the users that
// create-users provisions; usernameInfix, when set, narrows to that pool.
searchPrefix := loadtestutil.ScaleTestPrefix + "-"
if usernameInfix != "" {
searchPrefix += usernameInfix + "-"
}
users, err := getScaletestUsersWithPrefix(ctx, client, searchPrefix)
if err != nil {
return nil, nil, xerrors.Errorf("list scaletest users: %w", err)
}
@@ -529,10 +546,14 @@ func selectNotificationReuseUsers(ctx context.Context, client *codersdk.Client,
if total > 0 {
pct = float64(adminCount) / float64(total) * 100
}
hint := fmt.Sprintf("coder exp scaletest create-users --count %d --template-admin-percentage %.2f --no-cleanup", total, pct)
if usernameInfix != "" {
hint = fmt.Sprintf("coder exp scaletest create-users --count %d --template-admin-percentage %.2f --username-infix %q --no-cleanup", total, pct, usernameInfix)
}
return nil, nil, xerrors.Errorf(
"not enough scaletest users to reuse: found %d template admins and %d regular users, need %d and %d. "+
"Create them first, for example: coder exp scaletest create-users --count %d --template-admin-percentage %.2f --no-cleanup",
len(adminUsers), len(regularUsers), adminCount, regularCount, total, pct)
"Create them first, for example: %s",
len(adminUsers), len(regularUsers), adminCount, regularCount, hint)
}
// Token names are auto-generated by the server; we only need the returned key.