@@ -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,314 @@ 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
+ template string
861
+ noCleanup bool
862
+ noWaitForAgents bool
863
+
864
+ parameterFlags workspaceParameterFlags
865
+ tracingFlags = & scaletestTracingFlags {}
866
+ strategy = & scaletestStrategyFlags {}
867
+ cleanupStrategy = & scaletestStrategyFlags {cleanup :true }
868
+ output = & scaletestOutputFlags {}
869
+ prometheusFlags = & scaletestPrometheusFlags {}
870
+ )
871
+
872
+ client := new (codersdk.Client )
873
+
874
+ cmd := & serpent.Command {
875
+ Use :"coder-connect" ,
876
+ Short :"Simulate the load of Coder Desktop clients" ,
877
+ Middleware :serpent .Chain (
878
+ r .InitClient (client ),
879
+ ),
880
+ Handler :func (inv * serpent.Invocation )error {
881
+ ctx := inv .Context ()
882
+
883
+ notifyCtx ,stop := signal .NotifyContext (ctx ,StopSignals ... )// Checked later.
884
+ defer stop ()
885
+ ctx = notifyCtx
886
+
887
+ me ,err := requireAdmin (ctx ,client )
888
+ if err != nil {
889
+ return err
890
+ }
891
+
892
+ client .HTTPClient = & http.Client {
893
+ Transport :& codersdk.HeaderTransport {
894
+ Transport :http .DefaultTransport ,
895
+ Header :map [string ][]string {
896
+ codersdk .BypassRatelimitHeader : {"true" },
897
+ },
898
+ },
899
+ }
900
+
901
+ if workspaceCount <= 0 {
902
+ return xerrors .Errorf ("--workspace-count must be greater than 0" )
903
+ }
904
+ if powerUserWorkspaces <= 1 {
905
+ return xerrors .Errorf ("--power-user-workspaces must be greater than 1" )
906
+ }
907
+ if powerUserProportion < 0 || powerUserProportion > 100 {
908
+ return xerrors .Errorf ("--power-user-proportion must be between 0 and 100" )
909
+ }
910
+
911
+ if strategy .concurrency == 1 {
912
+ return xerrors .Errorf ("this test requires concurrent execution" )
913
+ }
914
+
915
+ powerUserWorkspaceCount := int64 (float64 (workspaceCount )* powerUserProportion / 100 )
916
+ remainder := powerUserWorkspaceCount % powerUserWorkspaces
917
+ if remainder != 0 {
918
+ workspaceCount -= remainder
919
+ powerUserWorkspaceCount -= remainder
920
+ }
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 ,"num_workspaces" ,"username" )
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
+ Metrics :metrics ,
1004
+ MetricLabelValues : []string {strconv .Itoa (int (powerUserWorkspaces ))},
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 (1 ),
1029
+ WorkspaceUpdatesTimeout :workspaceUpdatesTimeout ,
1030
+ Metrics :metrics ,
1031
+ MetricLabelValues : []string {strconv .Itoa (workspaceCount )},
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
+ config .MetricLabelValues = append (config .MetricLabelValues ,username )
1052
+
1053
+ var runner harness.Runnable = coderconnect .NewRunner (client ,config )
1054
+ if tracingEnabled {
1055
+ runner = & runnableTraceWrapper {
1056
+ tracer :tracer ,
1057
+ spanName :fmt .Sprintf ("%s/%s" ,name ,id ),
1058
+ runner :runner ,
1059
+ }
1060
+ }
1061
+
1062
+ th .AddRun (name ,id ,runner )
1063
+ }
1064
+
1065
+ _ ,_ = fmt .Fprintln (inv .Stderr ,"Running Coder Connect scaletest..." )
1066
+ testCtx ,testCancel := strategy .toContext (ctx )
1067
+ defer testCancel ()
1068
+ err = th .Run (testCtx )
1069
+ if err != nil {
1070
+ return xerrors .Errorf ("run test harness (harness failure, not a test failure): %w" ,err )
1071
+ }
1072
+
1073
+ // If the command was interrupted, skip stats.
1074
+ if notifyCtx .Err ()!= nil {
1075
+ return notifyCtx .Err ()
1076
+ }
1077
+
1078
+ res := th .Results ()
1079
+ for _ ,o := range outputs {
1080
+ err = o .write (res ,inv .Stdout )
1081
+ if err != nil {
1082
+ return xerrors .Errorf ("write output %q to %q: %w" ,o .format ,o .path ,err )
1083
+ }
1084
+ }
1085
+
1086
+ _ ,_ = fmt .Fprintln (inv .Stderr ,"\n Cleaning up..." )
1087
+ cleanupCtx ,cleanupCancel := cleanupStrategy .toContext (ctx )
1088
+ defer cleanupCancel ()
1089
+ err = th .Cleanup (cleanupCtx )
1090
+ if err != nil {
1091
+ return xerrors .Errorf ("cleanup tests: %w" ,err )
1092
+ }
1093
+
1094
+ if res .TotalFail > 0 {
1095
+ return xerrors .New ("load test failed, see above for more details" )
1096
+ }
1097
+
1098
+ return nil
1099
+ },
1100
+ }
1101
+
1102
+ cmd .Options = serpent.OptionSet {
1103
+ {
1104
+ Flag :"workspace-count" ,
1105
+ FlagShorthand :"c" ,
1106
+ Env :"CODER_SCALETEST_WORKSPACE_COUNT" ,
1107
+ Description :"Required: Total number of workspaces to create." ,
1108
+ Value :serpent .Int64Of (& workspaceCount ),
1109
+ },
1110
+ {
1111
+ Flag :"power-user-workspaces" ,
1112
+ Env :"CODER_SCALETEST_POWER_USER_WORKSPACES" ,
1113
+ Description :"Number of workspaces each power-user owns." ,
1114
+ Value :serpent .Int64Of (& powerUserWorkspaces ),
1115
+ Required :true ,
1116
+ },
1117
+ {
1118
+ Flag :"power-user-proportion" ,
1119
+ Env :"CODER_SCALETEST_POWER_USER_PROPORTION" ,
1120
+ Default :"50.0" ,
1121
+ Description :"Percentage of total workspaces owned by power-users (0-100)." ,
1122
+ Value :serpent .Float64Of (& powerUserProportion ),
1123
+ },
1124
+ {
1125
+ Flag :"workspace-updates-timeout" ,
1126
+ Env :"CODER_SCALETEST_WORKSPACE_UPDATES_TIMEOUT" ,
1127
+ Default :"5m" ,
1128
+ Description :"How long to wait for all expected workspace updates." ,
1129
+ Value :serpent .DurationOf (& workspaceUpdatesTimeout ),
1130
+ },
1131
+ {
1132
+ Flag :"template" ,
1133
+ FlagShorthand :"t" ,
1134
+ Env :"CODER_SCALETEST_TEMPLATE" ,
1135
+ Description :"Required: Name or ID of the template to use for workspaces." ,
1136
+ Value :serpent .StringOf (& template ),
1137
+ Required :true ,
1138
+ },
1139
+ {
1140
+ Flag :"no-wait-for-agents" ,
1141
+ Env :"CODER_SCALETEST_NO_WAIT_FOR_AGENTS" ,
1142
+ 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.` ,
1143
+ Value :serpent .BoolOf (& noWaitForAgents ),
1144
+ },
1145
+ {
1146
+ Flag :"no-cleanup" ,
1147
+ Env :"CODER_SCALETEST_NO_CLEANUP" ,
1148
+ Description :"Do not clean up resources after the test completes." ,
1149
+ Value :serpent .BoolOf (& noCleanup ),
1150
+ },
1151
+ }
1152
+
1153
+ cmd .Options = append (cmd .Options ,parameterFlags .cliParameters ()... )
1154
+ tracingFlags .attach (& cmd .Options )
1155
+ strategy .attach (& cmd .Options )
1156
+ cleanupStrategy .attach (& cmd .Options )
1157
+ output .attach (& cmd .Options )
1158
+ prometheusFlags .attach (& cmd .Options )
1159
+ return cmd
1160
+ }
1161
+
852
1162
func (r * RootCmd )scaletestWorkspaceTraffic ()* serpent.Command {
853
1163
var (
854
1164
tickInterval time.Duration