@@ -33,6 +33,7 @@ import (
33
33
"github.com/coder/coder/v2/codersdk"
34
34
"github.com/coder/coder/v2/codersdk/workspacesdk"
35
35
"github.com/coder/coder/v2/scaletest/agentconn"
36
+ "github.com/coder/coder/v2/scaletest/coderconnect"
36
37
"github.com/coder/coder/v2/scaletest/createworkspaces"
37
38
"github.com/coder/coder/v2/scaletest/dashboard"
38
39
"github.com/coder/coder/v2/scaletest/harness"
@@ -56,6 +57,7 @@ func (r *RootCmd) scaletestCmd() *serpent.Command {
56
57
r .scaletestCleanup (),
57
58
r .scaletestDashboard (),
58
59
r .scaletestCreateWorkspaces (),
60
+ r .scaletestCoderConnect (),
59
61
r .scaletestWorkspaceTraffic (),
60
62
},
61
63
}
@@ -132,10 +134,11 @@ func (s *scaletestTracingFlags) provider(ctx context.Context) (trace.TracerProvi
132
134
}
133
135
134
136
type scaletestStrategyFlags struct {
135
- cleanup bool
136
- concurrency int64
137
- timeout time.Duration
138
- timeoutPerJob time.Duration
137
+ cleanup bool
138
+ noConcurrencyFlag bool // for tests that require specific concurrency
139
+ concurrency int64
140
+ timeout time.Duration
141
+ timeoutPerJob time.Duration
139
142
}
140
143
141
144
func (s * scaletestStrategyFlags )attach (opts * serpent.OptionSet ) {
@@ -150,13 +153,6 @@ func (s *scaletestStrategyFlags) attach(opts *serpent.OptionSet) {
150
153
151
154
* opts = append (
152
155
* opts ,
153
- serpent.Option {
154
- Flag :concurrencyLong ,
155
- Env :concurrencyEnv ,
156
- Description :concurrencyDescription ,
157
- Default :"1" ,
158
- Value :serpent .Int64Of (& s .concurrency ),
159
- },
160
156
serpent.Option {
161
157
Flag :timeoutLong ,
162
158
Env :timeoutEnv ,
@@ -172,6 +168,16 @@ func (s *scaletestStrategyFlags) attach(opts *serpent.OptionSet) {
172
168
Value :serpent .DurationOf (& s .timeoutPerJob ),
173
169
},
174
170
)
171
+
172
+ if ! s .noConcurrencyFlag {
173
+ * opts = append (* opts , serpent.Option {
174
+ Flag :concurrencyLong ,
175
+ Env :concurrencyEnv ,
176
+ Description :concurrencyDescription ,
177
+ Default :"1" ,
178
+ Value :serpent .Int64Of (& s .concurrency ),
179
+ })
180
+ }
175
181
}
176
182
177
183
func (s * scaletestStrategyFlags )toStrategy () harness.ExecutionStrategy {
@@ -850,6 +856,315 @@ func (r *RootCmd) scaletestCreateWorkspaces() *serpent.Command {
850
856
return cmd
851
857
}
852
858
859
+ func (r * RootCmd )scaletestCoderConnect ()* serpent.Command {
860
+ var (
861
+ workspaceCount int64
862
+ powerUserWorkspaces int64
863
+ powerUserProportion float64
864
+ workspaceUpdatesTimeout time.Duration
865
+ dialTimeout time.Duration
866
+ template string
867
+ noCleanup bool
868
+ noWaitForAgents bool
869
+
870
+ parameterFlags workspaceParameterFlags
871
+ tracingFlags = & scaletestTracingFlags {}
872
+ // This test requires unlimited concurrency
873
+ strategy = & scaletestStrategyFlags {noConcurrencyFlag :true ,concurrency :0 }
874
+ cleanupStrategy = & scaletestStrategyFlags {cleanup :true }
875
+ output = & scaletestOutputFlags {}
876
+ prometheusFlags = & scaletestPrometheusFlags {}
877
+ )
878
+
879
+ cmd := & serpent.Command {
880
+ Use :"coder-connect" ,
881
+ Short :"Simulate the load of Coder Desktop clients" ,
882
+ Handler :func (inv * serpent.Invocation )error {
883
+ ctx := inv .Context ()
884
+ client ,err := r .TryInitClient (inv )
885
+ if err != nil {
886
+ return err
887
+ }
888
+
889
+ notifyCtx ,stop := signal .NotifyContext (ctx ,StopSignals ... )// Checked later.
890
+ defer stop ()
891
+ ctx = notifyCtx
892
+
893
+ me ,err := requireAdmin (ctx ,client )
894
+ if err != nil {
895
+ return err
896
+ }
897
+
898
+ client .HTTPClient = & http.Client {
899
+ Transport :& codersdk.HeaderTransport {
900
+ Transport :http .DefaultTransport ,
901
+ Header :map [string ][]string {
902
+ codersdk .BypassRatelimitHeader : {"true" },
903
+ },
904
+ },
905
+ }
906
+
907
+ if workspaceCount <= 0 {
908
+ return xerrors .Errorf ("--workspace-count must be greater than 0" )
909
+ }
910
+ if powerUserWorkspaces <= 1 {
911
+ return xerrors .Errorf ("--power-user-workspaces must be greater than 1" )
912
+ }
913
+ if powerUserProportion < 0 || powerUserProportion > 100 {
914
+ return xerrors .Errorf ("--power-user-proportion must be between 0 and 100" )
915
+ }
916
+
917
+ powerUserWorkspaceCount := int64 (float64 (workspaceCount )* powerUserProportion / 100 )
918
+ remainder := powerUserWorkspaceCount % powerUserWorkspaces
919
+ workspaceCount -= remainder
920
+ powerUserWorkspaceCount -= remainder
921
+ powerUserCount := powerUserWorkspaceCount / powerUserWorkspaces
922
+ regularWorkspaceCount := workspaceCount - powerUserWorkspaceCount
923
+ regularUserCount := regularWorkspaceCount
924
+
925
+ _ ,_ = fmt .Fprintf (inv .Stderr ,"Distribution plan:\n " )
926
+ _ ,_ = fmt .Fprintf (inv .Stderr ," Total workspaces: %d\n " ,workspaceCount )
927
+ _ ,_ = fmt .Fprintf (inv .Stderr ," Power users: %d (each owning %d workspaces = %d total)\n " ,
928
+ powerUserCount ,powerUserWorkspaces ,powerUserWorkspaceCount )
929
+ _ ,_ = fmt .Fprintf (inv .Stderr ," Regular users: %d (each owning 1 workspace = %d total)\n " ,
930
+ regularUserCount ,regularWorkspaceCount )
931
+
932
+ outputs ,err := output .parse ()
933
+ if err != nil {
934
+ return xerrors .Errorf ("could not parse --output flags" )
935
+ }
936
+
937
+ tpl ,err := parseTemplate (ctx ,client ,me .OrganizationIDs ,template )
938
+ if err != nil {
939
+ return xerrors .Errorf ("parse template: %w" ,err )
940
+ }
941
+
942
+ cliRichParameters ,err := asWorkspaceBuildParameters (parameterFlags .richParameters )
943
+ if err != nil {
944
+ return xerrors .Errorf ("can't parse given parameter values: %w" ,err )
945
+ }
946
+
947
+ richParameters ,err := prepWorkspaceBuild (inv ,client ,prepWorkspaceBuildArgs {
948
+ Action :WorkspaceCreate ,
949
+ TemplateVersionID :tpl .ActiveVersionID ,
950
+
951
+ RichParameterFile :parameterFlags .richParameterFile ,
952
+ RichParameters :cliRichParameters ,
953
+ })
954
+ _ = richParameters
955
+ if err != nil {
956
+ return xerrors .Errorf ("prepare build: %w" ,err )
957
+ }
958
+
959
+ tracerProvider ,closeTracing ,tracingEnabled ,err := tracingFlags .provider (ctx )
960
+ if err != nil {
961
+ return xerrors .Errorf ("create tracer provider: %w" ,err )
962
+ }
963
+ tracer := tracerProvider .Tracer (scaletestTracerName )
964
+
965
+ reg := prometheus .NewRegistry ()
966
+ metrics := coderconnect .NewMetrics (reg )
967
+
968
+ logger := inv .Logger
969
+ prometheusSrvClose := ServeHandler (ctx ,logger ,promhttp .HandlerFor (reg , promhttp.HandlerOpts {}),prometheusFlags .Address ,"prometheus" )
970
+ defer prometheusSrvClose ()
971
+
972
+ defer func () {
973
+ _ ,_ = fmt .Fprintln (inv .Stderr ,"\n Uploading traces..." )
974
+ if err := closeTracing (ctx );err != nil {
975
+ _ ,_ = fmt .Fprintf (inv .Stderr ,"\n Error uploading traces: %+v\n " ,err )
976
+ }
977
+ // Wait for prometheus metrics to be scraped
978
+ _ ,_ = fmt .Fprintf (inv .Stderr ,"Waiting %s for prometheus metrics to be scraped\n " ,prometheusFlags .Wait )
979
+ <- time .After (prometheusFlags .Wait )
980
+ }()
981
+
982
+ _ ,_ = fmt .Fprintln (inv .Stderr ,"Creating users..." )
983
+
984
+ dialBarrier := harness .NewBarrier (int (powerUserCount + regularUserCount ))
985
+
986
+ configs := make ([]coderconnect.Config ,0 ,powerUserCount + regularUserCount )
987
+
988
+ for i := int64 (0 );i < powerUserCount ;i ++ {
989
+ config := coderconnect.Config {
990
+ User : coderconnect.UserConfig {
991
+ OrganizationID :me .OrganizationIDs [0 ],
992
+ },
993
+ Workspace : workspacebuild.Config {
994
+ OrganizationID :me .OrganizationIDs [0 ],
995
+ Request : codersdk.CreateWorkspaceRequest {
996
+ TemplateID :tpl .ID ,
997
+ RichParameterValues :richParameters ,
998
+ },
999
+ NoWaitForAgents :noWaitForAgents ,
1000
+ },
1001
+ WorkspaceCount :powerUserWorkspaces ,
1002
+ WorkspaceUpdatesTimeout :workspaceUpdatesTimeout ,
1003
+ DialTimeout :dialTimeout ,
1004
+ Metrics :metrics ,
1005
+ NoCleanup :noCleanup ,
1006
+ DialBarrier :dialBarrier ,
1007
+ }
1008
+ if err := config .Validate ();err != nil {
1009
+ return xerrors .Errorf ("validate config: %w" ,err )
1010
+ }
1011
+ configs = append (configs ,config )
1012
+ }
1013
+
1014
+ for i := int64 (0 );i < regularUserCount ;i ++ {
1015
+ workspaceCount := 1
1016
+ config := coderconnect.Config {
1017
+ User : coderconnect.UserConfig {
1018
+ OrganizationID :me .OrganizationIDs [0 ],
1019
+ },
1020
+ Workspace : workspacebuild.Config {
1021
+ OrganizationID :me .OrganizationIDs [0 ],
1022
+ Request : codersdk.CreateWorkspaceRequest {
1023
+ TemplateID :tpl .ID ,
1024
+ RichParameterValues :richParameters ,
1025
+ },
1026
+ NoWaitForAgents :noWaitForAgents ,
1027
+ },
1028
+ WorkspaceCount :int64 (workspaceCount ),
1029
+ WorkspaceUpdatesTimeout :workspaceUpdatesTimeout ,
1030
+ DialTimeout :dialTimeout ,
1031
+ Metrics :metrics ,
1032
+ NoCleanup :noCleanup ,
1033
+ DialBarrier :dialBarrier ,
1034
+ }
1035
+ if err := config .Validate ();err != nil {
1036
+ return xerrors .Errorf ("validate config: %w" ,err )
1037
+ }
1038
+ configs = append (configs ,config )
1039
+ }
1040
+
1041
+ th := harness .NewTestHarness (strategy .toStrategy (),cleanupStrategy .toStrategy ())
1042
+ for i ,config := range configs {
1043
+ name := fmt .Sprintf ("coderconnect-%dw" ,config .WorkspaceCount )
1044
+ id := strconv .Itoa (i )
1045
+ username ,email ,err := loadtestutil .GenerateUserIdentifier (id )
1046
+ if err != nil {
1047
+ return xerrors .Errorf ("generate user identifier: %w" ,err )
1048
+ }
1049
+ config .User .Username = username
1050
+ config .User .Email = email
1051
+
1052
+ var runner harness.Runnable = coderconnect .NewRunner (client ,config )
1053
+ if tracingEnabled {
1054
+ runner = & runnableTraceWrapper {
1055
+ tracer :tracer ,
1056
+ spanName :fmt .Sprintf ("%s/%s" ,name ,id ),
1057
+ runner :runner ,
1058
+ }
1059
+ }
1060
+
1061
+ th .AddRun (name ,id ,runner )
1062
+ }
1063
+
1064
+ _ ,_ = fmt .Fprintln (inv .Stderr ,"Running Coder Connect scaletest..." )
1065
+ testCtx ,testCancel := strategy .toContext (ctx )
1066
+ defer testCancel ()
1067
+ err = th .Run (testCtx )
1068
+ if err != nil {
1069
+ return xerrors .Errorf ("run test harness (harness failure, not a test failure): %w" ,err )
1070
+ }
1071
+
1072
+ // If the command was interrupted, skip stats.
1073
+ if notifyCtx .Err ()!= nil {
1074
+ return notifyCtx .Err ()
1075
+ }
1076
+
1077
+ res := th .Results ()
1078
+ for _ ,o := range outputs {
1079
+ err = o .write (res ,inv .Stdout )
1080
+ if err != nil {
1081
+ return xerrors .Errorf ("write output %q to %q: %w" ,o .format ,o .path ,err )
1082
+ }
1083
+ }
1084
+
1085
+ _ ,_ = fmt .Fprintln (inv .Stderr ,"\n Cleaning up..." )
1086
+ cleanupCtx ,cleanupCancel := cleanupStrategy .toContext (ctx )
1087
+ defer cleanupCancel ()
1088
+ err = th .Cleanup (cleanupCtx )
1089
+ if err != nil {
1090
+ return xerrors .Errorf ("cleanup tests: %w" ,err )
1091
+ }
1092
+
1093
+ if res .TotalFail > 0 {
1094
+ return xerrors .New ("load test failed, see above for more details" )
1095
+ }
1096
+
1097
+ return nil
1098
+ },
1099
+ }
1100
+
1101
+ cmd .Options = serpent.OptionSet {
1102
+ {
1103
+ Flag :"workspace-count" ,
1104
+ FlagShorthand :"c" ,
1105
+ Env :"CODER_SCALETEST_WORKSPACE_COUNT" ,
1106
+ Description :"Required: Total number of workspaces to create." ,
1107
+ Value :serpent .Int64Of (& workspaceCount ),
1108
+ },
1109
+ {
1110
+ Flag :"power-user-workspaces" ,
1111
+ Env :"CODER_SCALETEST_POWER_USER_WORKSPACES" ,
1112
+ Description :"Number of workspaces each power-user owns." ,
1113
+ Value :serpent .Int64Of (& powerUserWorkspaces ),
1114
+ Required :true ,
1115
+ },
1116
+ {
1117
+ Flag :"power-user-proportion" ,
1118
+ Env :"CODER_SCALETEST_POWER_USER_PROPORTION" ,
1119
+ Default :"50.0" ,
1120
+ Description :"Percentage of total workspaces owned by power-users (0-100)." ,
1121
+ Value :serpent .Float64Of (& powerUserProportion ),
1122
+ },
1123
+ {
1124
+ Flag :"workspace-updates-timeout" ,
1125
+ Env :"CODER_SCALETEST_WORKSPACE_UPDATES_TIMEOUT" ,
1126
+ Default :"5m" ,
1127
+ Description :"How long to wait for all expected workspace updates." ,
1128
+ Value :serpent .DurationOf (& workspaceUpdatesTimeout ),
1129
+ },
1130
+ {
1131
+ Flag :"dial-timeout" ,
1132
+ Env :"CODER_SCALETEST_DIAL_TIMEOUT" ,
1133
+ Default :"2m" ,
1134
+ Description :"Timeout for dialing the Coder Connect endpoint." ,
1135
+ Value :serpent .DurationOf (& dialTimeout ),
1136
+ },
1137
+ {
1138
+ Flag :"template" ,
1139
+ FlagShorthand :"t" ,
1140
+ Env :"CODER_SCALETEST_TEMPLATE" ,
1141
+ Description :"Required: Name or ID of the template to use for workspaces." ,
1142
+ Value :serpent .StringOf (& template ),
1143
+ Required :true ,
1144
+ },
1145
+ {
1146
+ Flag :"no-wait-for-agents" ,
1147
+ Env :"CODER_SCALETEST_NO_WAIT_FOR_AGENTS" ,
1148
+ Description :`Do not wait for agents to start before marking the test as succeeded. This can be useful if you are running the test against a template that does not start the agent quickly.` ,
1149
+ Value :serpent .BoolOf (& noWaitForAgents ),
1150
+ },
1151
+ {
1152
+ Flag :"no-cleanup" ,
1153
+ Env :"CODER_SCALETEST_NO_CLEANUP" ,
1154
+ Description :"Do not clean up resources after the test completes." ,
1155
+ Value :serpent .BoolOf (& noCleanup ),
1156
+ },
1157
+ }
1158
+
1159
+ cmd .Options = append (cmd .Options ,parameterFlags .cliParameters ()... )
1160
+ tracingFlags .attach (& cmd .Options )
1161
+ strategy .attach (& cmd .Options )
1162
+ cleanupStrategy .attach (& cmd .Options )
1163
+ output .attach (& cmd .Options )
1164
+ prometheusFlags .attach (& cmd .Options )
1165
+ return cmd
1166
+ }
1167
+
853
1168
func (r * RootCmd )scaletestWorkspaceTraffic ()* serpent.Command {
854
1169
var (
855
1170
tickInterval time.Duration