github.com/bitcubate/cryptojournal@v1.2.5-0.20171102134152-f578b3d788ab/src/app/bootstrap.go (about) 1 package app 2 3 import ( 4 "crypto/rand" 5 "encoding/hex" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "log" 11 "os" 12 "os/exec" 13 "path" 14 "path/filepath" 15 "sort" 16 "strings" 17 "time" 18 19 "github.com/fragmenta/query" 20 ) 21 22 // TODO: Most of this should probably go into a config/bootstrap package within fragmenta? 23 // "github.com/fragmenta/fragmenta/config" 24 25 const ( 26 fragmentaVersion = "1.5" 27 permissions = 0744 28 createDatabaseMigrationName = "Create-Database" 29 createTablesMigrationName = "Create-Tables" 30 ) 31 32 var ( 33 // ConfigDevelopment holds the development config from fragmenta.json 34 ConfigDevelopment map[string]string 35 36 // ConfigProduction holds development config from fragmenta.json 37 ConfigProduction map[string]string 38 39 // ConfigTest holds the app test config from fragmenta.json 40 ConfigTest map[string]string 41 ) 42 43 // Bootstrap generates missing config files, sql migrations, and runs the first migrations 44 // For this we need to know what to call the app, but we default to bitcubate/cryptojournal for now 45 // we could use our current folder name? 46 func Bootstrap() error { 47 // We assume we're being run from root of project path 48 projectPath, err := os.Getwd() 49 if err != nil { 50 return err 51 } 52 53 fmt.Printf("\nBootstrapping server...\n") 54 55 err = generateConfig(projectPath) 56 if err != nil { 57 return err 58 } 59 60 err = generateCreateSQL(projectPath) 61 if err != nil { 62 return err 63 } 64 65 // Run the migrations without the fragmenta tool being present 66 err = runMigrations(projectPath) 67 if err != nil { 68 return err 69 } 70 71 return nil 72 } 73 74 // RequiresBootStrap returns true if the app requires bootstrapping 75 func RequiresBootStrap() bool { 76 if !fileExists(configPath()) { 77 return true 78 } 79 return false 80 } 81 82 func configPath() string { 83 return "secrets/fragmenta.json" 84 } 85 86 func projectPathRelative(projectPath string) string { 87 goSrc := os.Getenv("GOPATH") + "/src/" 88 return strings.Replace(projectPath, goSrc, "", 1) 89 } 90 91 func generateConfig(projectPath string) error { 92 configPath := configPath() 93 prefix := path.Base(projectPath) 94 log.Printf("Generating new config at %s", configPath) 95 96 ConfigProduction = map[string]string{} 97 ConfigDevelopment = map[string]string{} 98 ConfigTest = map[string]string{ 99 "port": "3000", 100 "log": "log/test.log", 101 "db_adapter": "postgres", 102 "db": prefix + "_test", 103 "db_user": prefix + "_server", 104 "db_pass": randomKey(8), 105 "assets_compiled": "no", 106 "path": projectPathRelative(projectPath), 107 "hmac_key": randomKey(32), 108 "secret_key": randomKey(32), 109 "session_name": prefix + "_session", 110 "meta_title": "", 111 "meta_desc": "", 112 "meta_keywords": "", 113 } 114 115 // Check if the psql binary is available, if not, assume the worst 116 // and setup for mysql instead 117 _, err := runCommand("psql", "--version") 118 if err != nil { 119 log.Printf("Generating config for MYSQL as no psql found") 120 121 ConfigTest["db_adapter"] = "mysql" 122 123 // Truncate username, yes really 124 // MySQL user names can be up to 16 characters long before MySQL 5.7.8) 125 if len(ConfigTest["db_user"]) > 16 { 126 ConfigTest["db_user"] = ConfigTest["db_user"][:16] 127 } 128 } 129 130 // Should we ask for db prefix when setting up? 131 // hmm, in fact can we do this setup here at all!! 132 for k, v := range ConfigTest { 133 ConfigDevelopment[k] = v 134 ConfigProduction[k] = v 135 } 136 ConfigDevelopment["db"] = prefix + "_development" 137 ConfigDevelopment["log"] = "log/development.log" 138 ConfigDevelopment["hmac_key"] = randomKey(32) 139 ConfigDevelopment["secret_key"] = randomKey(32) 140 141 ConfigProduction["db"] = prefix + "_production" 142 ConfigProduction["log"] = "log/production.log" 143 ConfigProduction["port"] = "80" 144 ConfigProduction["assets_compiled"] = "yes" 145 ConfigProduction["hmac_key"] = randomKey(32) 146 ConfigProduction["secret_key"] = randomKey(32) 147 148 configs := map[string]map[string]string{ 149 "production": ConfigProduction, 150 "development": ConfigDevelopment, 151 "test": ConfigTest, 152 } 153 154 configJSON, err := json.MarshalIndent(configs, "", "\t") 155 if err != nil { 156 log.Printf("Error parsing config %s %v", configPath, err) 157 return err 158 } 159 160 // Write the config json file 161 err = ioutil.WriteFile(configPath, configJSON, permissions) 162 if err != nil { 163 log.Printf("Error writing config %s %v", configPath, err) 164 return err 165 } 166 167 return nil 168 } 169 170 // generateCreateSQL generates an SQL migration file to create the database user and database referred to in config 171 func generateCreateSQL(projectPath string) error { 172 173 // Set up a Create-Database migration, which comes first 174 name := path.Base(projectPath) 175 d := ConfigDevelopment["db"] 176 u := ConfigDevelopment["db_user"] 177 p := ConfigDevelopment["db_pass"] 178 sql := fmt.Sprintf("/* Setup database for %s */\nCREATE USER \"%s\" WITH PASSWORD '%s';\nCREATE DATABASE \"%s\" WITH OWNER \"%s\";", name, u, p, d, u) 179 180 // Adjust sql for mysql dialect - we should be asking the db adapter for this really 181 if ConfigDevelopment["db_adapter"] == "mysql" { 182 sql = fmt.Sprintf(` 183 CREATE USER %s@localhost IDENTIFIED BY '%s'; 184 CREATE DATABASE %s; 185 grant all privileges on %s.* to %s@localhost; 186 `, u, p, d, d, u) 187 } 188 189 // Generate a migration to create db with today's date 190 file := migrationPath(projectPath, createDatabaseMigrationName) 191 err := ioutil.WriteFile(file, []byte(sql), 0744) 192 if err != nil { 193 return err 194 } 195 196 // If we have a Create-Tables file, copy it out to a new migration with today's date 197 createTablesPath := path.Join(projectPath, "db", "migrate", createTablesMigrationName+".sql.tmpl") 198 if fileExists(createTablesPath) { 199 sql, err := ioutil.ReadFile(createTablesPath) 200 if err != nil { 201 return err 202 } 203 204 // Now vivify the template, for now we just replace one key 205 sqlString := strings.Replace(string(sql), "[[.fragmenta_db_user]]", u, -1) 206 207 file = migrationPath(projectPath, createTablesMigrationName) 208 err = ioutil.WriteFile(file, []byte(sqlString), 0744) 209 if err != nil { 210 return err 211 } 212 // Remove the old file 213 os.Remove(createTablesPath) 214 215 } else { 216 fmt.Printf("NO TABLES %s", createTablesPath) 217 } 218 219 return nil 220 } 221 222 // runMigrations at projectPath 223 func runMigrations(projectPath string) error { 224 var migrations []string 225 var migrationCount int 226 227 config := ConfigDevelopment 228 229 // Get a list of migration files 230 files, err := filepath.Glob("./db/migrate/*.sql") 231 if err != nil { 232 return err 233 } 234 235 // Sort the list alphabetically 236 sort.Strings(files) 237 238 for _, file := range files { 239 filename := path.Base(file) 240 241 log.Printf("Running migration %s", filename) 242 243 args := []string{"-d", config["db"], "-f", file} 244 if strings.Contains(filename, createDatabaseMigrationName) { 245 args = []string{"-f", file} 246 log.Printf("Running database creation migration: %s", file) 247 } 248 249 // Execute this sql file against the database 250 result, err := runCommand("psql", args...) 251 if err != nil || strings.Contains(string(result), "ERROR") { 252 if err == nil { 253 err = fmt.Errorf("\n%s", string(result)) 254 } 255 256 // If at any point we fail, log it and break 257 log.Printf("ERROR loading sql migration:%s\n", err) 258 log.Printf("All further migrations cancelled\n\n") 259 return err 260 } 261 262 migrationCount++ 263 migrations = append(migrations, filename) 264 log.Printf("Completed migration %s\n%s\n%s", filename, string(result), "-") 265 266 } 267 268 if migrationCount > 0 { 269 writeMetadata(config, migrations) 270 log.Printf("Migrations complete up to migration %v on db %s\n\n", migrations, config["db"]) 271 } 272 273 return nil 274 } 275 276 // Oh, we need to write the full list of migrations, not just one migration version 277 278 // Update the database with a line recording what we have done 279 func writeMetadata(config map[string]string, migrations []string) { 280 // Try opening the db (db may not exist at this stage) 281 err := openDatabase(config) 282 if err != nil { 283 log.Printf("Database ERROR %s", err) 284 } 285 defer query.CloseDatabase() 286 287 for _, m := range migrations { 288 sql := "Insert into fragmenta_metadata(updated_at,fragmenta_version,migration_version,status) VALUES(NOW(),$1,$2,100);" 289 result, err := query.ExecSQL(sql, fragmentaVersion, m) 290 if err != nil { 291 log.Printf("Database ERROR %s %s", err, result) 292 } 293 } 294 295 } 296 297 // Open our database 298 func openDatabase(config map[string]string) error { 299 // Open the database 300 options := map[string]string{ 301 "adapter": config["db_adapter"], 302 "user": config["db_user"], 303 "password": config["db_pass"], 304 "db": config["db"], 305 "host": config["db_host"], 306 // "debug" : "true", 307 } 308 309 err := query.OpenDatabase(options) 310 if err != nil { 311 return err 312 } 313 314 log.Printf("%s\n", "-") 315 log.Printf("Opened database at %s for user %s", config["db"], config["db_user"]) 316 return nil 317 } 318 319 // Generate a suitable path for a migration from the current date/time down to nanosecond 320 func migrationPath(path string, name string) string { 321 now := time.Now() 322 layout := "2006-01-02-150405" 323 return fmt.Sprintf("%s/db/migrate/%s-%s.sql", path, now.Format(layout), name) 324 } 325 326 // Generate a random 32 byte key encoded in base64 327 func randomKey(l int64) string { 328 k := make([]byte, l) 329 if _, err := io.ReadFull(rand.Reader, k); err != nil { 330 return "" 331 } 332 return hex.EncodeToString(k) 333 } 334 335 // fileExists returns true if this file exists 336 func fileExists(p string) bool { 337 _, err := os.Stat(p) 338 if err != nil && os.IsNotExist(err) { 339 return false 340 } 341 342 return true 343 } 344 345 // runCommand runs a command with exec.Command 346 func runCommand(command string, args ...string) ([]byte, error) { 347 348 cmd := exec.Command(command, args...) 349 output, err := cmd.CombinedOutput() 350 if err != nil { 351 return output, err 352 } 353 354 return output, nil 355 }