@@ -65,9 +65,11 @@ import (
65
65
"github.com/coder/coder/coderd/httpapi"
66
66
"github.com/coder/coder/coderd/httpmw"
67
67
"github.com/coder/coder/coderd/prometheusmetrics"
68
+ "github.com/coder/coder/coderd/rbac"
68
69
"github.com/coder/coder/coderd/telemetry"
69
70
"github.com/coder/coder/coderd/tracing"
70
71
"github.com/coder/coder/coderd/updatecheck"
72
+ "github.com/coder/coder/coderd/userpassword"
71
73
"github.com/coder/coder/codersdk"
72
74
"github.com/coder/coder/cryptorand"
73
75
"github.com/coder/coder/provisioner/echo"
@@ -561,62 +563,13 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
561
563
options .Database = databasefake .New ()
562
564
options .Pubsub = database .NewPubsubInMemory ()
563
565
} else {
564
- logger .Debug (ctx , "connecting to postgresql" )
565
- sqlDB , err := sql .Open (sqlDriver , cfg .PostgresURL .Value )
566
+ sqlDB , err := connectToPostgres (ctx , logger , sqlDriver , cfg .PostgresURL .Value )
566
567
if err != nil {
567
- return xerrors .Errorf ("dial postgres: %w" , err )
568
+ return xerrors .Errorf ("connect to postgres: %w" , err )
568
569
}
569
- defer sqlDB .Close ()
570
-
571
- pingCtx , pingCancel := context .WithTimeout (ctx , 15 * time .Second )
572
- defer pingCancel ()
573
-
574
- err = sqlDB .PingContext (pingCtx )
575
- if err != nil {
576
- return xerrors .Errorf ("ping postgres: %w" , err )
577
- }
578
-
579
- // Ensure the PostgreSQL version is >=13.0.0!
580
- version , err := sqlDB .QueryContext (ctx , "SHOW server_version;" )
581
- if err != nil {
582
- return xerrors .Errorf ("get postgres version: %w" , err )
583
- }
584
- if ! version .Next () {
585
- return xerrors .Errorf ("no rows returned for version select" )
586
- }
587
- var versionStr string
588
- err = version .Scan (& versionStr )
589
- if err != nil {
590
- return xerrors .Errorf ("scan version: %w" , err )
591
- }
592
- _ = version .Close ()
593
- versionStr = strings .Split (versionStr , " " )[0 ]
594
- if semver .Compare ("v" + versionStr , "v13" ) < 0 {
595
- return xerrors .New ("PostgreSQL version must be v13.0.0 or higher!" )
596
- }
597
- logger .Debug (ctx , "connected to postgresql" , slog .F ("version" , versionStr ))
598
-
599
- err = migrations .Up (sqlDB )
600
- if err != nil {
601
- return xerrors .Errorf ("migrate up: %w" , err )
602
- }
603
- // The default is 0 but the request will fail with a 500 if the DB
604
- // cannot accept new connections, so we try to limit that here.
605
- // Requests will wait for a new connection instead of a hard error
606
- // if a limit is set.
607
- sqlDB .SetMaxOpenConns (10 )
608
- // Allow a max of 3 idle connections at a time. Lower values end up
609
- // creating a lot of connection churn. Since each connection uses about
610
- // 10MB of memory, we're allocating 30MB to Postgres connections per
611
- // replica, but is better than causing Postgres to spawn a thread 15-20
612
- // times/sec. PGBouncer's transaction pooling is not the greatest so
613
- // it's not optimal for us to deploy.
614
- //
615
- // This was set to 10 before we started doing HA deployments, but 3 was
616
- // later determined to be a better middle ground as to not use up all
617
- // of PGs default connection limit while simultaneously avoiding a lot
618
- // of connection churn.
619
- sqlDB .SetMaxIdleConns (3 )
570
+ defer func () {
571
+ _ = sqlDB .Close ()
572
+ }()
620
573
621
574
options .Database = database .New (sqlDB )
622
575
options .Pubsub , err = database .NewPubsub (ctx , sqlDB , cfg .PostgresURL .Value )
@@ -1005,7 +958,232 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
1005
958
postgresBuiltinURLCmd .Flags ().BoolVar (& pgRawURL , "raw-url" , false , "Output the raw connection URL instead of a psql command." )
1006
959
postgresBuiltinServeCmd .Flags ().BoolVar (& pgRawURL , "raw-url" , false , "Output the raw connection URL instead of a psql command." )
1007
960
1008
- root .AddCommand (postgresBuiltinURLCmd , postgresBuiltinServeCmd )
961
+ var (
962
+ newUserDBURL string
963
+ newUserSSHKeygenAlgorithm string
964
+ newUserUsername string
965
+ newUserEmail string
966
+ newUserPassword string
967
+ )
968
+ createAdminUserCommand := & cobra.Command {
969
+ Use : "create-admin-user" ,
970
+ Short : "Create a new admin user with the given username, email and password and adds it to every organization." ,
971
+ RunE : func (cmd * cobra.Command , args []string ) error {
972
+ ctx := cmd .Context ()
973
+
974
+ sshKeygenAlgorithm , err := gitsshkey .ParseAlgorithm (newUserSSHKeygenAlgorithm )
975
+ if err != nil {
976
+ return xerrors .Errorf ("parse ssh keygen algorithm %q: %w" , newUserSSHKeygenAlgorithm , err )
977
+ }
978
+
979
+ if val , exists := os .LookupEnv ("CODER_POSTGRES_URL" ); exists {
980
+ newUserDBURL = val
981
+ }
982
+ if val , exists := os .LookupEnv ("CODER_SSH_KEYGEN_ALGORITHM" ); exists {
983
+ newUserSSHKeygenAlgorithm = val
984
+ }
985
+ if val , exists := os .LookupEnv ("CODER_USERNAME" ); exists {
986
+ newUserUsername = val
987
+ }
988
+ if val , exists := os .LookupEnv ("CODER_EMAIL" ); exists {
989
+ newUserEmail = val
990
+ }
991
+ if val , exists := os .LookupEnv ("CODER_PASSWORD" ); exists {
992
+ newUserPassword = val
993
+ }
994
+
995
+ cfg := createConfig (cmd )
996
+ logger := slog .Make (sloghuman .Sink (cmd .ErrOrStderr ()))
997
+ if ok , _ := cmd .Flags ().GetBool (varVerbose ); ok {
998
+ logger = logger .Leveled (slog .LevelDebug )
999
+ }
1000
+
1001
+ ctx , cancel := signal .NotifyContext (ctx , InterruptSignals ... )
1002
+ defer cancel ()
1003
+
1004
+ if newUserDBURL == "" {
1005
+ cmd .Printf ("Using built-in PostgreSQL (%s)\n " , cfg .PostgresPath ())
1006
+ url , closePg , err := startBuiltinPostgres (ctx , cfg , logger )
1007
+ if err != nil {
1008
+ return err
1009
+ }
1010
+ defer func () {
1011
+ _ = closePg ()
1012
+ }()
1013
+ newUserDBURL = url
1014
+ }
1015
+
1016
+ sqlDB , err := connectToPostgres (ctx , logger , "postgres" , newUserDBURL )
1017
+ if err != nil {
1018
+ return xerrors .Errorf ("connect to postgres: %w" , err )
1019
+ }
1020
+ defer func () {
1021
+ _ = sqlDB .Close ()
1022
+ }()
1023
+ db := database .New (sqlDB )
1024
+
1025
+ validateInputs := func (username , email , password string ) error {
1026
+ // Use the validator tags so we match the API's validation.
1027
+ req := codersdk.CreateUserRequest {
1028
+ Username : "username" ,
1029
+ Email : "email@coder.com" ,
1030
+ Password : "ValidPa$$word123!" ,
1031
+ OrganizationID : uuid .New (),
1032
+ }
1033
+ if username != "" {
1034
+ req .Username = username
1035
+ }
1036
+ if email != "" {
1037
+ req .Email = email
1038
+ }
1039
+ if password != "" {
1040
+ req .Password = password
1041
+ }
1042
+
1043
+ return httpapi .Validate .Struct (req )
1044
+ }
1045
+
1046
+ if newUserUsername == "" {
1047
+ newUserUsername , err = cliui .Prompt (cmd , cliui.PromptOptions {
1048
+ Text : "Username" ,
1049
+ Validate : func (val string ) error {
1050
+ if val == "" {
1051
+ return xerrors .New ("username cannot be empty" )
1052
+ }
1053
+ return validateInputs (val , "" , "" )
1054
+ },
1055
+ })
1056
+ if err != nil {
1057
+ return err
1058
+ }
1059
+ }
1060
+ if newUserEmail == "" {
1061
+ newUserEmail , err = cliui .Prompt (cmd , cliui.PromptOptions {
1062
+ Text : "Email" ,
1063
+ Validate : func (val string ) error {
1064
+ if val == "" {
1065
+ return xerrors .New ("email cannot be empty" )
1066
+ }
1067
+ return validateInputs ("" , val , "" )
1068
+ },
1069
+ })
1070
+ if err != nil {
1071
+ return err
1072
+ }
1073
+ }
1074
+ if newUserPassword == "" {
1075
+ newUserPassword , err = cliui .Prompt (cmd , cliui.PromptOptions {
1076
+ Text : "Password" ,
1077
+ Secret : true ,
1078
+ Validate : func (val string ) error {
1079
+ if val == "" {
1080
+ return xerrors .New ("password cannot be empty" )
1081
+ }
1082
+ return validateInputs ("" , "" , val )
1083
+ },
1084
+ })
1085
+ if err != nil {
1086
+ return err
1087
+ }
1088
+
1089
+ // Prompt again.
1090
+ _ , err = cliui .Prompt (cmd , cliui.PromptOptions {
1091
+ Text : "Confirm password" ,
1092
+ Secret : true ,
1093
+ Validate : func (val string ) error {
1094
+ if val != newUserPassword {
1095
+ return xerrors .New ("passwords do not match" )
1096
+ }
1097
+ return nil
1098
+ },
1099
+ })
1100
+ if err != nil {
1101
+ return err
1102
+ }
1103
+ }
1104
+
1105
+ err = validateInputs (newUserUsername , newUserEmail , newUserPassword )
1106
+ if err != nil {
1107
+ return xerrors .Errorf ("validate inputs: %w" , err )
1108
+ }
1109
+
1110
+ hashedPassword , err := userpassword .Hash (newUserPassword )
1111
+ if err != nil {
1112
+ return xerrors .Errorf ("hash password: %w" , err )
1113
+ }
1114
+
1115
+ // Create the user.
1116
+ var newUser database.User
1117
+ err = db .InTx (func (tx database.Store ) error {
1118
+ orgs , err := tx .GetOrganizations (ctx )
1119
+ if err != nil {
1120
+ return xerrors .Errorf ("get organizations: %w" , err )
1121
+ }
1122
+
1123
+ newUser , err = tx .InsertUser (ctx , database.InsertUserParams {
1124
+ ID : uuid .New (),
1125
+ Email : newUserEmail ,
1126
+ Username : newUserUsername ,
1127
+ HashedPassword : []byte (hashedPassword ),
1128
+ CreatedAt : database .Now (),
1129
+ UpdatedAt : database .Now (),
1130
+ RBACRoles : []string {rbac .RoleOwner ()},
1131
+ LoginType : database .LoginTypePassword ,
1132
+ })
1133
+ if err != nil {
1134
+ return xerrors .Errorf ("insert user: %w" , err )
1135
+ }
1136
+
1137
+ privateKey , publicKey , err := gitsshkey .Generate (sshKeygenAlgorithm )
1138
+ if err != nil {
1139
+ return xerrors .Errorf ("generate user gitsshkey: %w" , err )
1140
+ }
1141
+ _ , err = tx .InsertGitSSHKey (ctx , database.InsertGitSSHKeyParams {
1142
+ UserID : newUser .ID ,
1143
+ CreatedAt : database .Now (),
1144
+ UpdatedAt : database .Now (),
1145
+ PrivateKey : privateKey ,
1146
+ PublicKey : publicKey ,
1147
+ })
1148
+ if err != nil {
1149
+ return xerrors .Errorf ("insert user gitsshkey: %w" , err )
1150
+ }
1151
+
1152
+ for _ , org := range orgs {
1153
+ _ , err := tx .InsertOrganizationMember (ctx , database.InsertOrganizationMemberParams {
1154
+ OrganizationID : org .ID ,
1155
+ UserID : newUser .ID ,
1156
+ CreatedAt : database .Now (),
1157
+ UpdatedAt : database .Now (),
1158
+ Roles : []string {rbac .RoleOrgAdmin (org .ID )},
1159
+ })
1160
+ if err != nil {
1161
+ return xerrors .Errorf ("insert organization member: %w" , err )
1162
+ }
1163
+ }
1164
+
1165
+ return nil
1166
+ }, nil )
1167
+ if err != nil {
1168
+ return err
1169
+ }
1170
+
1171
+ _ , _ = fmt .Fprintln (cmd .ErrOrStderr (), "User created successfully." )
1172
+ _ , _ = fmt .Fprintln (cmd .ErrOrStderr (), "ID: " + newUser .ID .String ())
1173
+ _ , _ = fmt .Fprintln (cmd .ErrOrStderr (), "Username: " + newUser .Username )
1174
+ _ , _ = fmt .Fprintln (cmd .ErrOrStderr (), "Email: " + newUser .Email )
1175
+ _ , _ = fmt .Fprintln (cmd .ErrOrStderr (), "Password: ********" )
1176
+
1177
+ return nil
1178
+ },
1179
+ }
1180
+ createAdminUserCommand .Flags ().StringVar (& newUserDBURL , "postgres-url" , "" , "URL of a PostgreSQL database. If empty, the built-in PostgreSQL deployment will be used (Coder must not be already running in this case). Consumes $CODER_POSTGRES_URL." )
1181
+ createAdminUserCommand .Flags ().StringVar (& newUserSSHKeygenAlgorithm , "ssh-keygen-algorithm" , "ed25519" , "The algorithm to use for generating ssh keys. Accepted values are \" ed25519\" , \" ecdsa\" , or \" rsa4096\" . Consumes $CODER_SSH_KEYGEN_ALGORITHM." )
1182
+ createAdminUserCommand .Flags ().StringVar (& newUserUsername , "username" , "" , "The username of the new user. If not specified, you will be prompted via stdin. Consumes $CODER_USERNAME." )
1183
+ createAdminUserCommand .Flags ().StringVar (& newUserEmail , "email" , "" , "The email of the new user. If not specified, you will be prompted via stdin. Consumes $CODER_EMAIL." )
1184
+ createAdminUserCommand .Flags ().StringVar (& newUserPassword , "password" , "" , "The password of the new user. If not specified, you will be prompted via stdin. Consumes $CODER_PASSWORD." )
1185
+
1186
+ root .AddCommand (postgresBuiltinURLCmd , postgresBuiltinServeCmd , createAdminUserCommand )
1009
1187
1010
1188
deployment .AttachFlags (root .Flags (), vip , false )
1011
1189
@@ -1560,3 +1738,71 @@ func buildLogger(cmd *cobra.Command, cfg *codersdk.DeploymentConfig) (slog.Logge
1560
1738
}
1561
1739
}, nil
1562
1740
}
1741
+
1742
+ func connectToPostgres (ctx context.Context , logger slog.Logger , driver string , dbURL string ) (* sql.DB , error ) {
1743
+ logger .Debug (ctx , "connecting to postgresql" )
1744
+ sqlDB , err := sql .Open (driver , dbURL )
1745
+ if err != nil {
1746
+ return nil , xerrors .Errorf ("dial postgres: %w" , err )
1747
+ }
1748
+
1749
+ ok := false
1750
+ defer func () {
1751
+ if ! ok {
1752
+ _ = sqlDB .Close ()
1753
+ }
1754
+ }()
1755
+
1756
+ pingCtx , pingCancel := context .WithTimeout (ctx , 15 * time .Second )
1757
+ defer pingCancel ()
1758
+
1759
+ err = sqlDB .PingContext (pingCtx )
1760
+ if err != nil {
1761
+ return nil , xerrors .Errorf ("ping postgres: %w" , err )
1762
+ }
1763
+
1764
+ // Ensure the PostgreSQL version is >=13.0.0!
1765
+ version , err := sqlDB .QueryContext (ctx , "SHOW server_version;" )
1766
+ if err != nil {
1767
+ return nil , xerrors .Errorf ("get postgres version: %w" , err )
1768
+ }
1769
+ if ! version .Next () {
1770
+ return nil , xerrors .Errorf ("no rows returned for version select" )
1771
+ }
1772
+ var versionStr string
1773
+ err = version .Scan (& versionStr )
1774
+ if err != nil {
1775
+ return nil , xerrors .Errorf ("scan version: %w" , err )
1776
+ }
1777
+ _ = version .Close ()
1778
+ versionStr = strings .Split (versionStr , " " )[0 ]
1779
+ if semver .Compare ("v" + versionStr , "v13" ) < 0 {
1780
+ return nil , xerrors .New ("PostgreSQL version must be v13.0.0 or higher!" )
1781
+ }
1782
+ logger .Debug (ctx , "connected to postgresql" , slog .F ("version" , versionStr ))
1783
+
1784
+ err = migrations .Up (sqlDB )
1785
+ if err != nil {
1786
+ return nil , xerrors .Errorf ("migrate up: %w" , err )
1787
+ }
1788
+ // The default is 0 but the request will fail with a 500 if the DB
1789
+ // cannot accept new connections, so we try to limit that here.
1790
+ // Requests will wait for a new connection instead of a hard error
1791
+ // if a limit is set.
1792
+ sqlDB .SetMaxOpenConns (10 )
1793
+ // Allow a max of 3 idle connections at a time. Lower values end up
1794
+ // creating a lot of connection churn. Since each connection uses about
1795
+ // 10MB of memory, we're allocating 30MB to Postgres connections per
1796
+ // replica, but is better than causing Postgres to spawn a thread 15-20
1797
+ // times/sec. PGBouncer's transaction pooling is not the greatest so
1798
+ // it's not optimal for us to deploy.
1799
+ //
1800
+ // This was set to 10 before we started doing HA deployments, but 3 was
1801
+ // later determined to be a better middle ground as to not use up all
1802
+ // of PGs default connection limit while simultaneously avoiding a lot
1803
+ // of connection churn.
1804
+ sqlDB .SetMaxIdleConns (3 )
1805
+
1806
+ ok = true
1807
+ return sqlDB , nil
1808
+ }
0 commit comments