mirror of
https://github.com/coder/coder.git
synced 2026-10-31 02:43:21 +08:00
feat(cli): add --username-infix selection to scaletest notifications… (#28515)
This commit is contained in:
+24
-8
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user