feat(scaletest/dashboard): integrate chromedp (#9927)

* Adds a set of actions to automatically interact with a Coder instance using chromedp
* Integrates the chromedp actions into the scaletest dashboard command,
* Re-enables the previously disabled unit tests for scaletest/dashboard
* Removes previous dashboard actions based around codersdk
This commit is contained in:
Cian Johnston 2023-10-02 10:40:17 +01:00 committed by GitHub
parent 1906cc4806
commit 1c48610d56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 490 additions and 604 deletions

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"strconv"
@ -1046,9 +1047,10 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *clibase.Cmd {
func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
var (
count int64
minWait time.Duration
maxWait time.Duration
interval time.Duration
jitter time.Duration
headless bool
randSeed int64
client = &codersdk.Client{}
tracingFlags = &scaletestTracingFlags{}
@ -1065,6 +1067,12 @@ func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
if !(interval > 0) {
return xerrors.Errorf("--interval must be greater than zero")
}
if !(jitter < interval) {
return xerrors.Errorf("--jitter must be less than --interval")
}
ctx := inv.Context()
logger := slog.Make(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelInfo)
tracerProvider, closeTracing, tracingEnabled, err := tracingFlags.provider(ctx)
@ -1094,19 +1102,42 @@ func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
th := harness.NewTestHarness(strategy.toStrategy(), cleanupStrategy.toStrategy())
for i := int64(0); i < count; i++ {
name := fmt.Sprintf("dashboard-%d", i)
config := dashboard.Config{
MinWait: minWait,
MaxWait: maxWait,
Trace: tracingEnabled,
Logger: logger.Named(name),
RollTable: dashboard.DefaultActions,
users, err := getScaletestUsers(ctx, client)
if err != nil {
return xerrors.Errorf("get scaletest users")
}
for _, usr := range users {
//nolint:gosec // not used for cryptographic purposes
rndGen := rand.New(rand.NewSource(randSeed))
name := fmt.Sprintf("dashboard-%s", usr.Username)
userTokResp, err := client.CreateToken(ctx, usr.ID.String(), codersdk.CreateTokenRequest{
Lifetime: 30 * 24 * time.Hour,
Scope: "",
TokenName: fmt.Sprintf("scaletest-%d", time.Now().Unix()),
})
if err != nil {
return xerrors.Errorf("create token for user: %w", err)
}
userClient := codersdk.New(client.URL)
userClient.SetSessionToken(userTokResp.Key)
config := dashboard.Config{
Interval: interval,
Jitter: jitter,
Trace: tracingEnabled,
Logger: logger.Named(name),
Headless: headless,
ActionFunc: dashboard.ClickRandomElement,
RandIntn: rndGen.Intn,
}
//nolint:gocritic
logger.Info(ctx, "runner config", slog.F("min_wait", interval), slog.F("max_wait", jitter), slog.F("headless", headless), slog.F("trace", tracingEnabled))
if err := config.Validate(); err != nil {
return err
}
var runner harness.Runnable = dashboard.NewRunner(client, metrics, config)
var runner harness.Runnable = dashboard.NewRunner(userClient, metrics, config)
if tracingEnabled {
runner = &runnableTraceWrapper{
tracer: tracer,
@ -1143,25 +1174,32 @@ func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
cmd.Options = []clibase.Option{
{
Flag: "count",
Env: "CODER_SCALETEST_DASHBOARD_COUNT",
Default: "1",
Description: "Number of concurrent workers.",
Value: clibase.Int64Of(&count),
Flag: "interval",
Env: "CODER_SCALETEST_DASHBOARD_INTERVAL",
Default: "3s",
Description: "Interval between actions.",
Value: clibase.DurationOf(&interval),
},
{
Flag: "min-wait",
Env: "CODER_SCALETEST_DASHBOARD_MIN_WAIT",
Default: "100ms",
Description: "Minimum wait between fetches.",
Value: clibase.DurationOf(&minWait),
Flag: "jitter",
Env: "CODER_SCALETEST_DASHBOARD_JITTER",
Default: "2s",
Description: "Jitter between actions.",
Value: clibase.DurationOf(&jitter),
},
{
Flag: "max-wait",
Env: "CODER_SCALETEST_DASHBOARD_MAX_WAIT",
Default: "1s",
Description: "Maximum wait between fetches.",
Value: clibase.DurationOf(&maxWait),
Flag: "headless",
Env: "CODER_SCALETEST_DASHBOARD_HEADLESS",
Default: "true",
Description: "Controls headless mode. Setting to false is useful for debugging.",
Value: clibase.BoolOf(&headless),
},
{
Flag: "rand-seed",
Env: "CODER_SCALETEST_DASHBOARD_RAND_SEED",
Default: "0",
Description: "Seed for the random number generator.",
Value: clibase.Int64Of(&randSeed),
},
}

View File

@ -92,28 +92,78 @@ func TestScaleTestWorkspaceTraffic(t *testing.T) {
// This test just validates that the CLI command accepts its known arguments.
func TestScaleTestDashboard(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancelFunc()
t.Run("MinWait", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancelFunc()
log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
client := coderdtest.New(t, &coderdtest.Options{
Logger: &log,
log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
client := coderdtest.New(t, &coderdtest.Options{
Logger: &log,
})
_ = coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "exp", "scaletest", "dashboard",
"--interval", "0s",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "--interval must be greater than zero")
})
_ = coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "exp", "scaletest", "dashboard",
"--count", "1",
"--min-wait", "100ms",
"--max-wait", "1s",
"--timeout", "5s",
"--scaletest-prometheus-address", "127.0.0.1:0",
"--scaletest-prometheus-wait", "0s",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
t.Run("MaxWait", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancelFunc()
err := inv.WithContext(ctx).Run()
require.NoError(t, err, "")
log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
client := coderdtest.New(t, &coderdtest.Options{
Logger: &log,
})
_ = coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "exp", "scaletest", "dashboard",
"--interval", "1s",
"--jitter", "1s",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "--jitter must be less than --interval")
})
t.Run("OK", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancelFunc()
log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
client := coderdtest.New(t, &coderdtest.Options{
Logger: &log,
})
_ = coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "exp", "scaletest", "dashboard",
"--interval", "1s",
"--jitter", "500ms",
"--timeout", "5s",
"--scaletest-prometheus-address", "127.0.0.1:0",
"--scaletest-prometheus-wait", "0s",
"--rand-seed", "1234567890",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err := inv.WithContext(ctx).Run()
require.NoError(t, err, "")
})
}

6
go.mod
View File

@ -244,6 +244,9 @@ require (
github.com/bep/godartsass/v2 v2.0.0 // indirect
github.com/bep/golibsass v1.1.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 // indirect
github.com/chromedp/chromedp v0.9.2 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/containerd/continuity v0.4.2-0.20230616210509-1e0d26eb2381 // indirect
@ -273,6 +276,9 @@ require (
github.com/go-test/deep v1.0.8 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect

36
go.sum
View File

@ -87,6 +87,8 @@ github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
@ -109,6 +111,7 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
@ -184,6 +187,7 @@ github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqy
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@ -194,6 +198,12 @@ github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 h1:aPflPkRFkVwbW6dmcVqfgwp1i+UWGFH6VgR1Jim5Ygc=
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw=
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
@ -295,6 +305,7 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
@ -322,6 +333,7 @@ github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a h1:fwNLHrP5Rbg/mGSXCjtPdpbqv2GucVTA/KMi8wEm6mE=
github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a/go.mod h1:/WeFVhhxMOGypVKS0w8DUJxUBbHypnWkUVnW7p5c9Pw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
@ -347,6 +359,7 @@ github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@ -387,6 +400,7 @@ github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QX
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
@ -396,10 +410,13 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -524,6 +541,7 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU=
github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI=
@ -570,6 +588,7 @@ github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3s
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY=
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -579,6 +598,7 @@ github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 h1:+aAGyK41KRn8jbF2Q7PLL0Sxwg6dShGcQSeCC7nZQ8E=
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedib0t/go-pretty/v6 v6.4.0 h1:YlI/2zYDrweA4MThiYMKtGRfT+2qZOO65ulej8GTcVI=
github.com/jedib0t/go-pretty/v6 v6.4.0/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@ -595,14 +615,17 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jsimonetti/rtnetlink v1.3.2 h1:dcn0uWkfxycEEyNy0IGfx3GrhQ38LH7odjxAghimsVI=
github.com/jsimonetti/rtnetlink v1.3.2/go.mod h1:BBu4jZCpTjP6Gk0/wfrO8qcqymnN3g0hoFqObRmUo6U=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=
github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
@ -637,6 +660,7 @@ github.com/kylecarbs/terraform-config-inspect v0.0.0-20211215004401-bbc517866b88
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
@ -671,10 +695,12 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
@ -688,6 +714,7 @@ github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
@ -707,8 +734,10 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
@ -716,6 +745,7 @@ github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKt
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek=
github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o=
@ -736,6 +766,7 @@ github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJ
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4=
github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg=
github.com/outcaste-io/ristretto v0.2.1/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac=
@ -790,12 +821,14 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM=
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg=
github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI=
@ -806,6 +839,7 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
@ -927,6 +961,7 @@ github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGj
github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.14.0 h1:/Xrd39K7DXbHzlisFP9c4pHao4yyf+/Ug9LEz+Y/yhc=
github.com/zclconf/go-cty v1.14.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
@ -1391,6 +1426,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,97 +0,0 @@
package dashboard
import (
"context"
"math/rand"
"sync"
"github.com/coder/coder/v2/codersdk"
)
type cache struct {
sync.RWMutex
workspaces []codersdk.Workspace
templates []codersdk.Template
users []codersdk.User
}
func (c *cache) fill(ctx context.Context, client *codersdk.Client) error {
c.Lock()
defer c.Unlock()
me, err := client.User(ctx, codersdk.Me)
if err != nil {
return err
}
ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
if err != nil {
return err
}
c.workspaces = ws.Workspaces
tpl, err := client.TemplatesByOrganization(ctx, me.OrganizationIDs[0])
if err != nil {
return err
}
c.templates = tpl
users, err := client.Users(ctx, codersdk.UsersRequest{})
if err != nil {
return err
}
c.users = users.Users
return nil
}
func (c *cache) setWorkspaces(ws []codersdk.Workspace) {
c.Lock()
c.workspaces = ws
c.Unlock()
}
func (c *cache) setTemplates(t []codersdk.Template) {
c.Lock()
c.templates = t
c.Unlock()
}
func (c *cache) randWorkspace() codersdk.Workspace {
c.RLock()
defer c.RUnlock()
if len(c.workspaces) == 0 {
return codersdk.Workspace{}
}
return pick(c.workspaces)
}
func (c *cache) randTemplate() codersdk.Template {
c.RLock()
defer c.RUnlock()
if len(c.templates) == 0 {
return codersdk.Template{}
}
return pick(c.templates)
}
func (c *cache) setUsers(u []codersdk.User) {
c.Lock()
c.users = u
c.Unlock()
}
func (c *cache) randUser() codersdk.User {
c.RLock()
defer c.RUnlock()
if len(c.users) == 0 {
return codersdk.User{}
}
return pick(c.users)
}
// pick chooses a random element from a slice.
// If the slice is empty, it returns the zero value of the type.
func pick[T any](s []T) T {
if len(s) == 0 {
var zero T
return zero
}
// nolint:gosec
return s[rand.Intn(len(s))]
}

View File

@ -0,0 +1,221 @@
package dashboard
import (
"context"
"net/url"
"os"
"time"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
"golang.org/x/xerrors"
"cdr.dev/slog"
)
// Action is just a function that does something.
type Action func(ctx context.Context) error
// Selector locates an element on a page.
type Selector string
// Target is a thing that can be clicked.
type Target struct {
// Label is a human-readable label for the target.
Label Label
// ClickOn is the selector that locates the element to be clicked.
ClickOn Selector
// WaitFor is a selector that is expected to appear after the target is clicked.
WaitFor Selector
}
// Label identifies an action.
type Label string
var defaultTargets = []Target{
{
Label: "workspace_list",
ClickOn: `nav a[href="/workspaces"]:not(.active)`,
WaitFor: `tr[role="button"][data-testid^="workspace-"]`,
},
{
Label: "starter_templates",
ClickOn: `a[href="/starter-templates"]`,
WaitFor: `a[href^="/starter-templates/"]`,
},
{
Label: "workspace_details",
ClickOn: `tr[role="button"][data-testid^="workspace-"]`,
WaitFor: `tr[role="button"][data-testid^="build-"]`,
},
{
Label: "workspace_build_details",
ClickOn: `tr[role="button"][data-testid^="build-"]`,
WaitFor: `*[aria-label="Build details"]`,
},
{
Label: "template_list",
ClickOn: `nav a[href="/templates"]:not(.active)`,
WaitFor: `tr[role="button"][data-testid^="template-"]`,
},
{
Label: "template_docs",
ClickOn: `a[href^="/templates/"][href$="/docs"]:not([aria-current])`,
WaitFor: `#readme`,
},
{
Label: "template_files",
ClickOn: `a[href^="/templates/"][href$="/docs"]:not([aria-current])`,
WaitFor: `.monaco-editor`,
},
{
Label: "template_versions",
ClickOn: `a[href^="/templates/"][href$="/versions"]:not([aria-current])`,
WaitFor: `tr[role="button"][data-testid^="version-"]`,
},
{
Label: "template_version_details",
ClickOn: `tr[role="button"][data-testid^="version-"]`,
WaitFor: `.monaco-editor`,
},
{
Label: "user_list",
ClickOn: `nav a[href^="/users"]:not(.active)`,
WaitFor: `tr[data-testid^="user-"]`,
},
}
// ClickRandomElement returns an action that will click an element from defaultTargets.
// If no elements are found, an error is returned.
// If more than one element is found, one is chosen at random.
// The label of the clicked element is returned.
func ClickRandomElement(ctx context.Context, randIntn func(int) int) (Label, Action, error) {
var xpath Selector
var found bool
var err error
matches := make([]Target, 0)
for _, tgt := range defaultTargets {
xpath, found, err = randMatch(ctx, tgt.ClickOn, randIntn)
if err != nil {
return "", nil, xerrors.Errorf("find matches for %q: %w", tgt.ClickOn, err)
}
if !found {
continue
}
matches = append(matches, Target{
Label: tgt.Label,
ClickOn: xpath,
WaitFor: tgt.WaitFor,
})
}
if len(matches) == 0 {
return "", nil, xerrors.Errorf("no matches found")
}
match := pick(matches, randIntn)
// rely on map iteration order being random
act := func(actx context.Context) error {
if err := clickAndWait(actx, match.ClickOn, match.WaitFor); err != nil {
return xerrors.Errorf("click %q: %w", match.ClickOn, err)
}
return nil
}
return match.Label, act, nil
}
// randMatch returns a random match for the given selector.
// The returned selector is the full XPath of the matched node.
// If no matches are found, an error is returned.
// If multiple matches are found, one is chosen at random.
func randMatch(ctx context.Context, s Selector, randIntn func(int) int) (Selector, bool, error) {
var nodes []*cdp.Node
err := chromedp.Run(ctx, chromedp.Nodes(s, &nodes, chromedp.NodeVisible, chromedp.AtLeast(0)))
if err != nil {
return "", false, xerrors.Errorf("get nodes for selector %q: %w", s, err)
}
if len(nodes) == 0 {
return "", false, nil
}
n := pick(nodes, randIntn)
return Selector(n.FullXPath()), true, nil
}
// clickAndWait clicks the given selector and waits for the page to finish loading.
// The page is considered loaded when the network event "LoadingFinished" is received.
func clickAndWait(ctx context.Context, clickOn, waitFor Selector) error {
return chromedp.Run(ctx, chromedp.Tasks{
chromedp.Click(clickOn, chromedp.NodeVisible),
chromedp.WaitVisible(waitFor, chromedp.NodeVisible),
})
}
// initChromeDPCtx initializes a chromedp context with the given session token cookie
//
//nolint:revive // yes, headless is a control flag
func initChromeDPCtx(ctx context.Context, log slog.Logger, u *url.URL, sessionToken string, headless bool) (context.Context, context.CancelFunc, error) {
dir, err := os.MkdirTemp("", "scaletest-dashboard-*")
if err != nil {
return nil, nil, err
}
allocOpts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.UserDataDir(dir),
chromedp.DisableGPU,
)
if !headless { // headless is the default
allocOpts = append(allocOpts, chromedp.Flag("headless", false))
}
allocCtx, allocCtxCancel := chromedp.NewExecAllocator(ctx, allocOpts...)
cdpCtx, cdpCancel := chromedp.NewContext(allocCtx)
cancelFunc := func() {
cdpCancel()
allocCtxCancel()
if err := os.RemoveAll(dir); err != nil {
log.Error(ctx, "failed to remove temp user data dir", slog.F("dir", dir), slog.Error(err))
}
}
// set cookies
if err := setSessionTokenCookie(cdpCtx, sessionToken, u.Host); err != nil {
cancelFunc()
return nil, nil, xerrors.Errorf("set session token cookie: %w", err)
}
// visit main page
if err := visitMainPage(cdpCtx, u); err != nil {
cancelFunc()
return nil, nil, xerrors.Errorf("visit main page: %w", err)
}
return cdpCtx, cancelFunc, nil
}
func setSessionTokenCookie(ctx context.Context, token, domain string) error {
exp := cdp.TimeSinceEpoch(time.Now().Add(24 * time.Hour))
err := chromedp.Run(ctx, network.SetCookie("coder_session_token", token).
WithExpires(&exp).
WithDomain(domain).
WithHTTPOnly(false))
if err != nil {
return xerrors.Errorf("set coder_session_token cookie: %w", err)
}
return nil
}
func visitMainPage(ctx context.Context, u *url.URL) error {
return chromedp.Run(ctx, chromedp.Navigate(u.String()))
}
// pick chooses a random element from a slice.
// If the slice is empty, it returns the zero value of the type.
func pick[T any](s []T, randIntn func(int) int) T {
if len(s) == 0 {
var zero T
return zero
}
// nolint:gosec
return s[randIntn(len(s))]
}

View File

@ -1,37 +1,46 @@
package dashboard
import (
"context"
"time"
"golang.org/x/xerrors"
"cdr.dev/slog"
"golang.org/x/xerrors"
)
type Config struct {
// MinWait is the minimum interval between fetches.
MinWait time.Duration `json:"duration_min"`
// MaxWait is the maximum interval between fetches.
MaxWait time.Duration `json:"duration_max"`
// Interval is the minimum interval between fetches.
Interval time.Duration `json:"interval"`
// Jitter is the maximum interval between fetches.
Jitter time.Duration `json:"jitter"`
// Trace is whether to trace the requests.
Trace bool `json:"trace"`
// Logger is the logger to use.
Logger slog.Logger `json:"-"`
// RollTable is the set of actions to perform
RollTable RollTable `json:"roll_table"`
// Headless controls headless mode for chromedp.
Headless bool `json:"headless"`
// ActionFunc is a function that returns an action to run.
ActionFunc func(ctx context.Context, randIntn func(int) int) (Label, Action, error) `json:"-"`
// RandIntn is a function that returns a random number between 0 and n-1.
RandIntn func(int) int `json:"-"`
}
func (c Config) Validate() error {
if c.MinWait <= 0 {
return xerrors.Errorf("validate duration_min: must be greater than zero")
if !(c.Interval > 0) {
return xerrors.Errorf("validate interval: must be greater than zero")
}
if c.MaxWait <= 0 {
return xerrors.Errorf("validate duration_max: must be greater than zero")
if !(c.Jitter < c.Interval) {
return xerrors.Errorf("validate jitter: must be less than interval")
}
if c.MinWait > c.MaxWait {
return xerrors.Errorf("validate duration_min: must be less than duration_max")
if c.ActionFunc == nil {
return xerrors.Errorf("validate action func: must not be nil")
}
if c.RandIntn == nil {
return xerrors.Errorf("validate rand intn: must not be nil")
}
return nil

View File

@ -9,13 +9,11 @@ import (
type Metrics interface {
ObserveDuration(action string, d time.Duration)
IncErrors(action string)
IncStatuses(action string, code string)
}
type PromMetrics struct {
durationSeconds *prometheus.HistogramVec
errors *prometheus.CounterVec
statuses *prometheus.CounterVec
}
func NewMetrics(reg prometheus.Registerer) *PromMetrics {
@ -30,16 +28,10 @@ func NewMetrics(reg prometheus.Registerer) *PromMetrics {
Subsystem: "scaletest_dashboard",
Name: "errors_total",
}, []string{"action"}),
statuses: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "coderd",
Subsystem: "scaletest_dashboard",
Name: "statuses_total",
}, []string{"action", "code"}),
}
reg.MustRegister(m.durationSeconds)
reg.MustRegister(m.errors)
reg.MustRegister(m.statuses)
return m
}
@ -50,7 +42,3 @@ func (p *PromMetrics) ObserveDuration(action string, d time.Duration) {
func (p *PromMetrics) IncErrors(action string) {
p.errors.WithLabelValues(action).Inc()
}
func (p *PromMetrics) IncStatuses(action string, code string) {
p.statuses.WithLabelValues(action, code).Inc()
}

View File

@ -1,304 +0,0 @@
package dashboard
import (
"context"
"github.com/google/uuid"
"github.com/coder/coder/v2/codersdk"
)
// DefaultActions is a table of actions to perform.
// D&D nerds will feel right at home here :-)
// Note that the order of the table is important!
// Entries must be in ascending order.
var DefaultActions RollTable = []RollTableEntry{
{0, fetchWorkspaces, "fetch workspaces"},
{1, fetchUsers, "fetch users"},
{2, fetchTemplates, "fetch templates"},
{3, authCheckAsOwner, "authcheck owner"},
{4, authCheckAsNonOwner, "authcheck not owner"},
{5, fetchAuditLog, "fetch audit log"},
{6, fetchActiveUsers, "fetch active users"},
{7, fetchSuspendedUsers, "fetch suspended users"},
{8, fetchTemplateVersion, "fetch template version"},
{9, fetchWorkspace, "fetch workspace"},
{10, fetchTemplate, "fetch template"},
{11, fetchUserByID, "fetch user by ID"},
{12, fetchUserByUsername, "fetch user by username"},
{13, fetchWorkspaceBuild, "fetch workspace build"},
{14, fetchDeploymentConfig, "fetch deployment config"},
{15, fetchWorkspaceQuotaForUser, "fetch workspace quota for user"},
{16, fetchDeploymentStats, "fetch deployment stats"},
{17, fetchWorkspaceLogs, "fetch workspace logs"},
}
// RollTable is a slice of rollTableEntry.
type RollTable []RollTableEntry
// RollTableEntry is an entry in the roll table.
type RollTableEntry struct {
// Roll is the minimum number required to perform the action.
Roll int
// Fn is the function to call.
Fn func(ctx context.Context, p *Params) error
// Label is used for logging.
Label string
}
// choose returns the first entry in the table that is greater than or equal to n.
func (r RollTable) choose(n int) RollTableEntry {
for _, entry := range r {
if entry.Roll >= n {
return entry
}
}
return RollTableEntry{}
}
// max returns the maximum roll in the table.
// Important: this assumes that the table is sorted in ascending order.
func (r RollTable) max() int {
return r[len(r)-1].Roll
}
// Params is a set of parameters to pass to the actions in a rollTable.
type Params struct {
// client is the client to use for performing the action.
client *codersdk.Client
// me is the currently authenticated user. Lots of actions require this.
me codersdk.User
// For picking random resource IDs, we need to know what resources are
// present. We store them in a cache to avoid fetching them every time.
// This may seem counter-intuitive for load testing, but we want to avoid
// muddying results.
c *cache
}
// fetchWorkspaces fetches all workspaces.
func fetchWorkspaces(ctx context.Context, p *Params) error {
ws, err := p.client.Workspaces(ctx, codersdk.WorkspaceFilter{})
if err != nil {
// store the workspaces for later use in case they change
p.c.setWorkspaces(ws.Workspaces)
}
return err
}
// fetchUsers fetches all users.
func fetchUsers(ctx context.Context, p *Params) error {
users, err := p.client.Users(ctx, codersdk.UsersRequest{})
if err != nil {
p.c.setUsers(users.Users)
}
return err
}
// fetchActiveUsers fetches all active users
func fetchActiveUsers(ctx context.Context, p *Params) error {
_, err := p.client.Users(ctx, codersdk.UsersRequest{
Status: codersdk.UserStatusActive,
})
return err
}
// fetchSuspendedUsers fetches all suspended users
func fetchSuspendedUsers(ctx context.Context, p *Params) error {
_, err := p.client.Users(ctx, codersdk.UsersRequest{
Status: codersdk.UserStatusSuspended,
})
return err
}
// fetchTemplates fetches all templates.
func fetchTemplates(ctx context.Context, p *Params) error {
templates, err := p.client.TemplatesByOrganization(ctx, p.me.OrganizationIDs[0])
if err != nil {
p.c.setTemplates(templates)
}
return err
}
// fetchTemplateBuild fetches a single template version at random.
func fetchTemplateVersion(ctx context.Context, p *Params) error {
t := p.c.randTemplate()
_, err := p.client.TemplateVersion(ctx, t.ActiveVersionID)
return err
}
// fetchWorkspace fetches a single workspace at random.
func fetchWorkspace(ctx context.Context, p *Params) error {
w := p.c.randWorkspace()
_, err := p.client.WorkspaceByOwnerAndName(ctx, w.OwnerName, w.Name, codersdk.WorkspaceOptions{})
return err
}
// fetchWorkspaceBuild fetches a single workspace build at random.
func fetchWorkspaceBuild(ctx context.Context, p *Params) error {
w := p.c.randWorkspace()
_, err := p.client.WorkspaceBuild(ctx, w.LatestBuild.ID)
return err
}
// fetchTemplate fetches a single template at random.
func fetchTemplate(ctx context.Context, p *Params) error {
t := p.c.randTemplate()
_, err := p.client.Template(ctx, t.ID)
return err
}
// fetchUserByID fetches a single user at random by ID.
func fetchUserByID(ctx context.Context, p *Params) error {
u := p.c.randUser()
_, err := p.client.User(ctx, u.ID.String())
return err
}
// fetchUserByUsername fetches a single user at random by username.
func fetchUserByUsername(ctx context.Context, p *Params) error {
u := p.c.randUser()
_, err := p.client.User(ctx, u.Username)
return err
}
// fetchDeploymentConfig fetches the deployment config.
func fetchDeploymentConfig(ctx context.Context, p *Params) error {
_, err := p.client.DeploymentConfig(ctx)
return err
}
// fetchWorkspaceQuotaForUser fetches the workspace quota for a random user.
func fetchWorkspaceQuotaForUser(ctx context.Context, p *Params) error {
u := p.c.randUser()
_, err := p.client.WorkspaceQuota(ctx, u.ID.String())
return err
}
// fetchDeploymentStats fetches the deployment stats.
func fetchDeploymentStats(ctx context.Context, p *Params) error {
_, err := p.client.DeploymentStats(ctx)
return err
}
// fetchWorkspaceLogs fetches the logs for a random workspace.
func fetchWorkspaceLogs(ctx context.Context, p *Params) error {
w := p.c.randWorkspace()
ch, closer, err := p.client.WorkspaceBuildLogsAfter(ctx, w.LatestBuild.ID, 0)
if err != nil {
return err
}
defer func() {
_ = closer.Close()
}()
// Drain the channel.
for {
select {
case <-ctx.Done():
return ctx.Err()
case l, ok := <-ch:
if !ok {
return nil
}
_ = l
}
}
}
// fetchAuditLog fetches the audit log.
// As not all users have access to the audit log, we check first.
func fetchAuditLog(ctx context.Context, p *Params) error {
res, err := p.client.AuthCheck(ctx, codersdk.AuthorizationRequest{
Checks: map[string]codersdk.AuthorizationCheck{
"auditlog": {
Object: codersdk.AuthorizationObject{
ResourceType: codersdk.ResourceAuditLog,
},
Action: codersdk.ActionRead,
},
},
})
if err != nil {
return err
}
if !res["auditlog"] {
return nil // we are not authorized to read the audit log
}
// Fetch the first 25 audit log entries.
_, err = p.client.AuditLogs(ctx, codersdk.AuditLogsRequest{
Pagination: codersdk.Pagination{
Offset: 0,
Limit: 25,
},
})
return err
}
// authCheckAsOwner performs an auth check as the owner of a random
// resource type and action.
func authCheckAsOwner(ctx context.Context, p *Params) error {
_, err := p.client.AuthCheck(ctx, randAuthReq(
ownedBy(p.me.ID),
withAction(randAction()),
withObjType(randObjectType()),
inOrg(p.me.OrganizationIDs[0]),
))
return err
}
// authCheckAsNonOwner performs an auth check as a non-owner of a random
// resource type and action.
func authCheckAsNonOwner(ctx context.Context, p *Params) error {
_, err := p.client.AuthCheck(ctx, randAuthReq(
ownedBy(uuid.New()),
withAction(randAction()),
withObjType(randObjectType()),
inOrg(p.me.OrganizationIDs[0]),
))
return err
}
// nolint: gosec
func randAuthReq(mut ...func(*codersdk.AuthorizationCheck)) codersdk.AuthorizationRequest {
var check codersdk.AuthorizationCheck
for _, m := range mut {
m(&check)
}
return codersdk.AuthorizationRequest{
Checks: map[string]codersdk.AuthorizationCheck{
"check": check,
},
}
}
func ownedBy(myID uuid.UUID) func(check *codersdk.AuthorizationCheck) {
return func(check *codersdk.AuthorizationCheck) {
check.Object.OwnerID = myID.String()
}
}
func inOrg(orgID uuid.UUID) func(check *codersdk.AuthorizationCheck) {
return func(check *codersdk.AuthorizationCheck) {
check.Object.OrganizationID = orgID.String()
}
}
func withObjType(objType codersdk.RBACResource) func(check *codersdk.AuthorizationCheck) {
return func(check *codersdk.AuthorizationCheck) {
check.Object.ResourceType = objType
}
}
func withAction(action string) func(check *codersdk.AuthorizationCheck) {
return func(check *codersdk.AuthorizationCheck) {
check.Action = action
}
}
func randAction() string {
return pick(codersdk.AllRBACActions)
}
func randObjectType() codersdk.RBACResource {
return pick(codersdk.AllRBACResources)
}

View File

@ -1,17 +0,0 @@
package dashboard
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_allActions_ordering(t *testing.T) {
t.Parallel()
last := -1
for idx, entry := range DefaultActions {
require.Greater(t, entry.Roll, last, "roll table must be in ascending order, entry %d is out of order", idx)
last = entry.Roll
}
}

View File

@ -2,9 +2,7 @@ package dashboard
import (
"context"
"fmt"
"io"
"math/rand"
"time"
"golang.org/x/xerrors"
@ -35,46 +33,54 @@ func NewRunner(client *codersdk.Client, metrics Metrics, cfg Config) *Runner {
}
func (r *Runner) Run(ctx context.Context, _ string, _ io.Writer) error {
if r.client == nil {
return xerrors.Errorf("client is nil")
}
me, err := r.client.User(ctx, codersdk.Me)
if err != nil {
return err
return xerrors.Errorf("get scaletest user: %w", err)
}
//nolint:gocritic
r.cfg.Logger.Info(ctx, "running as user", slog.F("username", me.Username))
if len(me.OrganizationIDs) == 0 {
return xerrors.Errorf("user has no organizations")
}
c := &cache{}
if err := c.fill(ctx, r.client); err != nil {
return err
cdpCtx, cdpCancel, err := initChromeDPCtx(ctx, r.cfg.Logger, r.client.URL, r.client.SessionToken(), r.cfg.Headless)
if err != nil {
return xerrors.Errorf("init chromedp ctx: %w", err)
}
p := &Params{
client: r.client,
me: me,
c: c,
}
rolls := make(chan int)
go func() {
t := time.NewTicker(r.randWait())
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
rolls <- rand.Intn(r.cfg.RollTable.max() + 1) // nolint:gosec
t.Reset(r.randWait())
}
}
}()
defer cdpCancel()
t := time.NewTicker(1) // First one should be immediate
defer t.Stop()
for {
select {
case <-ctx.Done():
case <-cdpCtx.Done():
return nil
case n := <-rolls:
act := r.cfg.RollTable.choose(n)
go r.do(ctx, act, p)
case <-t.C:
var offset time.Duration
if r.cfg.Jitter > 0 {
offset = time.Duration(r.cfg.RandIntn(int(2*r.cfg.Jitter)) - int(r.cfg.Jitter))
}
wait := r.cfg.Interval + offset
t.Reset(wait)
l, act, err := r.cfg.ActionFunc(cdpCtx, r.cfg.RandIntn)
if err != nil {
r.cfg.Logger.Error(ctx, "calling ActionFunc", slog.Error(err))
continue
}
start := time.Now()
err = act(cdpCtx)
elapsed := time.Since(start)
r.metrics.ObserveDuration(string(l), elapsed)
if err != nil {
r.metrics.IncErrors(string(l))
//nolint:gocritic
r.cfg.Logger.Error(ctx, "action failed", slog.F("label", l), slog.Error(err))
} else {
//nolint:gocritic
r.cfg.Logger.Info(ctx, "action success", slog.F("label", l))
}
}
}
}
@ -82,50 +88,3 @@ func (r *Runner) Run(ctx context.Context, _ string, _ io.Writer) error {
func (*Runner) Cleanup(_ context.Context, _ string) error {
return nil
}
func (r *Runner) do(ctx context.Context, act RollTableEntry, p *Params) {
select {
case <-ctx.Done():
r.cfg.Logger.Info(ctx, "context done, stopping")
return
default:
var errored bool
cancelCtx, cancel := context.WithTimeout(ctx, r.cfg.MaxWait)
defer cancel()
start := time.Now()
err := act.Fn(cancelCtx, p)
cancel()
elapsed := time.Since(start)
if err != nil {
errored = true
r.cfg.Logger.Error( //nolint:gocritic
ctx, "action failed",
slog.Error(err),
slog.F("action", act.Label),
slog.F("elapsed", elapsed),
)
} else {
r.cfg.Logger.Info(ctx, "completed successfully",
slog.F("action", act.Label),
slog.F("elapsed", elapsed),
)
}
codeLabel := "200"
if apiErr, ok := codersdk.AsError(err); ok {
codeLabel = fmt.Sprintf("%d", apiErr.StatusCode())
} else if xerrors.Is(err, context.Canceled) {
codeLabel = "timeout"
}
r.metrics.ObserveDuration(act.Label, elapsed)
r.metrics.IncStatuses(act.Label, codeLabel)
if errored {
r.metrics.IncErrors(act.Label)
}
}
}
func (r *Runner) randWait() time.Duration {
// nolint:gosec // This is not for cryptographic purposes. Chill, gosec. Chill.
wait := time.Duration(rand.Intn(int(r.cfg.MaxWait) - int(r.cfg.MinWait)))
return r.cfg.MinWait + wait
}

View File

@ -2,6 +2,7 @@ package dashboard_test
import (
"context"
"math/rand"
"runtime"
"sync"
"testing"
@ -9,7 +10,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/coderdtest"
@ -19,7 +19,6 @@ import (
func Test_Run(t *testing.T) {
t.Parallel()
t.Skip("To be fixed by https://github.com/coder/coder/issues/9131")
if testutil.RaceEnabled() {
t.Skip("skipping timing-sensitive test because of race detector")
}
@ -27,35 +26,38 @@ func Test_Run(t *testing.T) {
t.Skip("skipping test on Windows")
}
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
successfulAction := func(context.Context, *dashboard.Params) error {
successAction := func(_ context.Context) error {
<-time.After(testutil.IntervalFast)
return nil
}
failingAction := func(context.Context, *dashboard.Params) error {
return xerrors.Errorf("failed")
}
hangingAction := func(ctx context.Context, _ *dashboard.Params) error {
<-ctx.Done()
return ctx.Err()
failAction := func(_ context.Context) error {
<-time.After(testutil.IntervalMedium)
return assert.AnError
}
testActions := []dashboard.RollTableEntry{
{0, successfulAction, "succeeds"},
{1, failingAction, "fails"},
{2, hangingAction, "hangs"},
}
//nolint: gosec // just for testing
rg := rand.New(rand.NewSource(0)) // deterministic for testing
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
log := slogtest.Make(t, &slogtest.Options{
IgnoreErrors: true,
})
m := &testMetrics{}
cfg := dashboard.Config{
MinWait: time.Millisecond,
MaxWait: 10 * time.Millisecond,
Logger: log,
RollTable: testActions,
Interval: 500 * time.Millisecond,
Jitter: 100 * time.Millisecond,
Logger: log,
Headless: true,
ActionFunc: func(_ context.Context, rnd func(int) int) (dashboard.Label, dashboard.Action, error) {
if rnd(2) == 0 {
return "fails", failAction, nil
}
return "succeeds", successAction, nil
},
RandIntn: rg.Intn,
}
r := dashboard.NewRunner(client, m, cfg)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
@ -69,23 +71,14 @@ func Test_Run(t *testing.T) {
assert.True(t, ok)
require.NoError(t, err)
if assert.NotEmpty(t, m.ObservedDurations["succeeds"]) {
assert.NotZero(t, m.ObservedDurations["succeeds"][0])
for _, dur := range m.ObservedDurations["succeeds"] {
assert.NotZero(t, dur)
}
if assert.NotEmpty(t, m.ObservedDurations["fails"]) {
assert.NotZero(t, m.ObservedDurations["fails"][0])
}
if assert.NotEmpty(t, m.ObservedDurations["hangs"]) {
assert.GreaterOrEqual(t, m.ObservedDurations["hangs"][0], cfg.MaxWait.Seconds())
for _, dur := range m.ObservedDurations["fails"] {
assert.NotZero(t, dur)
}
assert.Zero(t, m.Errors["succeeds"])
assert.NotZero(t, m.Errors["fails"])
assert.NotZero(t, m.Errors["hangs"])
assert.NotEmpty(t, m.Statuses["succeeds"])
assert.NotEmpty(t, m.Statuses["fails"])
assert.NotEmpty(t, m.Statuses["hangs"])
}
type testMetrics struct {

View File

@ -114,6 +114,10 @@ func (r *Results) PrintText(w io.Writer) {
}
_, _ = fmt.Fprintln(w, "\n\nTest results:")
if r.TotalRuns == 0 {
_, _ = fmt.Fprintln(w, "\tNo tests run")
return
}
_, _ = fmt.Fprintf(w, "\tPass: %d\n", r.TotalPass)
_, _ = fmt.Fprintf(w, "\tFail: %d\n", r.TotalFail)
_, _ = fmt.Fprintf(w, "\tTotal: %d\n", r.TotalRuns)

View File

@ -155,7 +155,7 @@ export const UsersTableBody: FC<
: sortRoles(user.roles);
return (
<TableRow key={user.id}>
<TableRow key={user.id} data-testid={`user-${user.id}`}>
<TableCell>
<AvatarData
title={user.username}