mirror of https://github.com/coder/coder.git
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:
parent
1906cc4806
commit
1c48610d56
|
@ -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),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
6
go.mod
|
@ -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
36
go.sum
|
@ -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=
|
||||
|
|
|
@ -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))]
|
||||
}
|
|
@ -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))]
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in New Issue