@@ -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
}
@@ -849,6 +851,320 @@ func (r *RootCmd) scaletestCreateWorkspaces() *serpent.Command {
849
851
return cmd
850
852
}
851
853
854
+ func (r * RootCmd )scaletestCoderConnect ()* serpent.Command {
855
+ var (
856
+ workspaceCount int64
857
+ powerUserWorkspaces int64
858
+ powerUserProportion float64
859
+ workspaceUpdatesTimeout time.Duration
860
+ dialTimeout time.Duration
861
+ template string
862
+ noCleanup bool
863
+ noWaitForAgents bool
864
+
865
+ parameterFlags workspaceParameterFlags
866
+ tracingFlags = & scaletestTracingFlags {}
867
+ strategy = & scaletestStrategyFlags {}
868
+ cleanupStrategy = & scaletestStrategyFlags {cleanup :true }
869
+ output = & scaletestOutputFlags {}
870
+ prometheusFlags = & scaletestPrometheusFlags {}
871
+ )
872
+
873
+ client := new (codersdk.Client )
874
+
875
+ cmd := & serpent.Command {
876
+ Use :"coder-connect" ,
877
+ Short :"Simulate the load of Coder Desktop clients" ,
878
+ Middleware :serpent .Chain (
879
+ r .InitClient (client ),
880
+ ),
881
+ Handler :func (inv * serpent.Invocation )error {
882
+ ctx := inv .Context ()
883
+
884
+ notifyCtx ,stop := signal .NotifyContext (ctx ,StopSignals ... )// Checked later.
885
+ defer stop ()
886
+ ctx = notifyCtx
887
+
888
+ me ,err := requireAdmin (ctx ,client )
889
+ if err != nil {
890
+ return err
891
+ }
892
+
893
+ client .HTTPClient = & http.Client {
894
+ Transport :& codersdk.HeaderTransport {
895
+ Transport :http .DefaultTransport ,
896
+ Header :map [string ][]string {
897
+ codersdk .BypassRatelimitHeader : {"true" },
898
+ },
899
+ },
900
+ }
901
+
902
+ if workspaceCount <= 0 {
903
+ return xerrors .Errorf ("--workspace-count must be greater than 0" )
904
+ }
905
+ if powerUserWorkspaces <= 1 {
906
+ return xerrors .Errorf ("--power-user-workspaces must be greater than 1" )
907
+ }
908
+ if powerUserProportion < 0 || powerUserProportion > 100 {
909
+ return xerrors .Errorf ("--power-user-proportion must be between 0 and 100" )
910
+ }
911
+
912
+ if strategy .concurrency == 1 {
913
+ return xerrors .Errorf ("this test requires concurrent execution" )
914
+ }
915
+
916
+ powerUserWorkspaceCount := int64 (float64 (workspaceCount )* powerUserProportion / 100 )
917
+ remainder := powerUserWorkspaceCount % powerUserWorkspaces
918
+ workspaceCount -= remainder
919
+ powerUserWorkspaceCount -= remainder
920
+ powerUserCount := powerUserWorkspaceCount / powerUserWorkspaces
921
+ regularWorkspaceCount := workspaceCount - powerUserWorkspaceCount
922
+ regularUserCount := regularWorkspaceCount
923
+
924
+ _ ,_ = fmt .Fprintf (inv .Stderr ,"Distribution plan:\n " )
925
+ _ ,_ = fmt .Fprintf (inv .Stderr ," Total workspaces: %d\n " ,workspaceCount )
926
+ _ ,_ = fmt .Fprintf (inv .Stderr ," Power users: %d (each owning %d workspaces = %d total)\n " ,
927
+ powerUserCount ,powerUserWorkspaces ,powerUserWorkspaceCount )
928
+ _ ,_ = fmt .Fprintf (inv .Stderr ," Regular users: %d (each owning 1 workspace = %d total)\n " ,
929
+ regularUserCount ,regularWorkspaceCount )
930
+
931
+ outputs ,err := output .parse ()
932
+ if err != nil {
933
+ return xerrors .Errorf ("could not parse --output flags" )
934
+ }
935
+
936
+ tpl ,err := parseTemplate (ctx ,client ,me .OrganizationIDs ,template )
937
+ if err != nil {
938
+ return xerrors .Errorf ("parse template: %w" ,err )
939
+ }
940
+
941
+ cliRichParameters ,err := asWorkspaceBuildParameters (parameterFlags .richParameters )
942
+ if err != nil {
943
+ return xerrors .Errorf ("can't parse given parameter values: %w" ,err )
944
+ }
945
+
946
+ richParameters ,err := prepWorkspaceBuild (inv ,client ,prepWorkspaceBuildArgs {
947
+ Action :WorkspaceCreate ,
948
+ TemplateVersionID :tpl .ActiveVersionID ,
949
+
950
+ RichParameterFile :parameterFlags .richParameterFile ,
951
+ RichParameters :cliRichParameters ,
952
+ })
953
+ _ = richParameters
954
+ if err != nil {
955
+ return xerrors .Errorf ("prepare build: %w" ,err )
956
+ }
957
+
958
+ tracerProvider ,closeTracing ,tracingEnabled ,err := tracingFlags .provider (ctx )
959
+ if err != nil {
960
+ return xerrors .Errorf ("create tracer provider: %w" ,err )
961
+ }
962
+ tracer := tracerProvider .Tracer (scaletestTracerName )
963
+
964
+ reg := prometheus .NewRegistry ()
965
+ metrics := coderconnect .NewMetrics (reg ,"num_workspaces" ,"username" )
966
+
967
+ logger := inv .Logger
968
+ prometheusSrvClose := ServeHandler (ctx ,logger ,promhttp .HandlerFor (reg , promhttp.HandlerOpts {}),prometheusFlags .Address ,"prometheus" )
969
+ defer prometheusSrvClose ()
970
+
971
+ defer func () {
972
+ _ ,_ = fmt .Fprintln (inv .Stderr ,"\n Uploading traces..." )
973
+ if err := closeTracing (ctx );err != nil {
974
+ _ ,_ = fmt .Fprintf (inv .Stderr ,"\n Error uploading traces: %+v\n " ,err )
975
+ }
976
+ // Wait for prometheus metrics to be scraped
977
+ _ ,_ = fmt .Fprintf (inv .Stderr ,"Waiting %s for prometheus metrics to be scraped\n " ,prometheusFlags .Wait )
978
+ <- time .After (prometheusFlags .Wait )
979
+ }()
980
+
981
+ _ ,_ = fmt .Fprintln (inv .Stderr ,"Creating users..." )
982
+
983
+ dialBarrier := harness .NewBarrier (int (powerUserCount + regularUserCount ))
984
+
985
+ configs := make ([]coderconnect.Config ,0 ,powerUserCount + regularUserCount )
986
+
987
+ for i := int64 (0 );i < powerUserCount ;i ++ {
988
+ config := coderconnect.Config {
989
+ User : coderconnect.UserConfig {
990
+ OrganizationID :me .OrganizationIDs [0 ],
991
+ },
992
+ Workspace : workspacebuild.Config {
993
+ OrganizationID :me .OrganizationIDs [0 ],
994
+ Request : codersdk.CreateWorkspaceRequest {
995
+ TemplateID :tpl .ID ,
996
+ RichParameterValues :richParameters ,
997
+ },
998
+ NoWaitForAgents :noWaitForAgents ,
999
+ },
1000
+ WorkspaceCount :powerUserWorkspaces ,
1001
+ WorkspaceUpdatesTimeout :workspaceUpdatesTimeout ,
1002
+ Metrics :metrics ,
1003
+ MetricLabelValues : []string {strconv .Itoa (int (powerUserWorkspaces ))},
1004
+ NoCleanup :noCleanup ,
1005
+ DialBarrier :dialBarrier ,
1006
+ }
1007
+ if err := config .Validate ();err != nil {
1008
+ return xerrors .Errorf ("validate config: %w" ,err )
1009
+ }
1010
+ configs = append (configs ,config )
1011
+ }
1012
+
1013
+ for i := int64 (0 );i < regularUserCount ;i ++ {
1014
+ workspaceCount := 1
1015
+ config := coderconnect.Config {
1016
+ User : coderconnect.UserConfig {
1017
+ OrganizationID :me .OrganizationIDs [0 ],
1018
+ },
1019
+ Workspace : workspacebuild.Config {
1020
+ OrganizationID :me .OrganizationIDs [0 ],
1021
+ Request : codersdk.CreateWorkspaceRequest {
1022
+ TemplateID :tpl .ID ,
1023
+ RichParameterValues :richParameters ,
1024
+ },
1025
+ NoWaitForAgents :noWaitForAgents ,
1026
+ },
1027
+ WorkspaceCount :int64 (1 ),
1028
+ WorkspaceUpdatesTimeout :workspaceUpdatesTimeout ,
1029
+ Metrics :metrics ,
1030
+ MetricLabelValues : []string {strconv .Itoa (workspaceCount )},
1031
+ NoCleanup :noCleanup ,
1032
+ DialBarrier :dialBarrier ,
1033
+ }
1034
+ if err := config .Validate ();err != nil {
1035
+ return xerrors .Errorf ("validate config: %w" ,err )
1036
+ }
1037
+ configs = append (configs ,config )
1038
+ }
1039
+
1040
+ th := harness .NewTestHarness (strategy .toStrategy (),cleanupStrategy .toStrategy ())
1041
+ for i ,config := range configs {
1042
+ name := fmt .Sprintf ("coderconnect-%dw" ,config .WorkspaceCount )
1043
+ id := strconv .Itoa (i )
1044
+ username ,email ,err := loadtestutil .GenerateUserIdentifier (id )
1045
+ if err != nil {
1046
+ return xerrors .Errorf ("generate user identifier: %w" ,err )
1047
+ }
1048
+ config .User .Username = username
1049
+ config .User .Email = email
1050
+ config .MetricLabelValues = append (config .MetricLabelValues ,username )
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
+
852
1168
func (r * RootCmd )scaletestWorkspaceTraffic ()* serpent.Command {
853
1169
var (
854
1170
tickInterval time.Duration