github.com/fragmenta/fragmenta-cms@v1.5.5/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: This should probably go into a bootstrap package within fragmenta? 23 24 const ( 25 fragmentaVersion = "1.2" 26 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 fragmenta-cms 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 prefix = strings.Replace(prefix, "-", "_", -1) 95 log.Printf("Generating new config at %s", configPath) 96 97 ConfigProduction = map[string]string{} 98 ConfigDevelopment = map[string]string{} 99 ConfigTest = map[string]string{ 100 "port": "3000", 101 "log": "log/test.log", 102 "db_adapter": "postgres", 103 "db": prefix + "_test", 104 "db_user": prefix + "_server", 105 "db_pass": randomKey(8), 106 "assets_compiled": "no", 107 "path": projectPathRelative(projectPath), 108 "hmac_key": randomKey(32), 109 "secret_key": randomKey(32), 110 "session_name": prefix, 111 } 112 113 for k, v := range ConfigTest { 114 ConfigDevelopment[k] = v 115 ConfigProduction[k] = v 116 } 117 ConfigDevelopment["db"] = prefix + "_development" 118 ConfigDevelopment["log"] = "log/development.log" 119 ConfigDevelopment["hmac_key"] = randomKey(32) 120 ConfigDevelopment["secret_key"] = randomKey(32) 121 122 ConfigProduction["db"] = prefix + "_production" 123 ConfigProduction["log"] = "log/production.log" 124 ConfigProduction["port"] = "80" //FIXME set up for https with port 443 125 ConfigProduction["assets_compiled"] = "yes" 126 ConfigProduction["hmac_key"] = randomKey(32) 127 ConfigProduction["secret_key"] = randomKey(32) 128 129 configs := map[string]map[string]string{ 130 "production": ConfigProduction, 131 "development": ConfigDevelopment, 132 "test": ConfigTest, 133 } 134 135 configJSON, err := json.MarshalIndent(configs, "", "\t") 136 if err != nil { 137 log.Printf("Error parsing config %s %v", configPath, err) 138 return err 139 } 140 141 // Write the config json file 142 err = ioutil.WriteFile(configPath, configJSON, permissions) 143 if err != nil { 144 log.Printf("Error writing config %s %v", configPath, err) 145 return err 146 } 147 148 return nil 149 } 150 151 // generateCreateSQL generates an SQL migration file to create the database user and database referred to in config 152 func generateCreateSQL(projectPath string) error { 153 154 // Set up a Create-Database migration, which comes first 155 name := path.Base(projectPath) 156 d := ConfigDevelopment["db"] 157 u := ConfigDevelopment["db_user"] 158 p := ConfigDevelopment["db_pass"] 159 sql := fmt.Sprintf("/* Setup database for %s */\nCREATE USER \"%s\" WITH PASSWORD '%s';\nCREATE DATABASE \"%s\" WITH OWNER \"%s\";", name, u, p, d, u) 160 161 // Generate a migration to create db with today's date 162 file := migrationPath(projectPath, createDatabaseMigrationName) 163 err := ioutil.WriteFile(file, []byte(sql), 0744) 164 if err != nil { 165 return err 166 } 167 168 // If we have a Create-Tables file, copy it out to a new migration with today's date 169 createTablesPath := path.Join(projectPath, "db", "migrate", createTablesMigrationName+".sql.tmpl") 170 if fileExists(createTablesPath) { 171 sql, err := ioutil.ReadFile(createTablesPath) 172 if err != nil { 173 return err 174 } 175 176 // Now vivify the template, for now we just replace one key 177 sqlString := strings.Replace(string(sql), "[[.fragmenta_db_user]]", u, -1) 178 179 file = migrationPath(projectPath, createTablesMigrationName) 180 err = ioutil.WriteFile(file, []byte(sqlString), 0744) 181 if err != nil { 182 return err 183 } 184 // Remove the old file 185 os.Remove(createTablesPath) 186 187 } else { 188 fmt.Printf("NO TABLES %s", createTablesPath) 189 } 190 191 return nil 192 } 193 194 // runMigrations at projectPath 195 func runMigrations(projectPath string) error { 196 var migrations []string 197 var migrationCount int 198 199 config := ConfigDevelopment 200 201 // Get a list of migration files 202 files, err := filepath.Glob("./db/migrate/*.sql") 203 if err != nil { 204 return err 205 } 206 207 // Sort the list alphabetically 208 sort.Strings(files) 209 210 for _, file := range files { 211 filename := path.Base(file) 212 213 log.Printf("Running migration %s", filename) 214 215 args := []string{"-d", config["db"], "-f", file} 216 if strings.Contains(filename, createDatabaseMigrationName) { 217 args = []string{"-f", file} 218 log.Printf("Running database creation migration: %s", file) 219 } 220 221 // Execute this sql file against the database 222 result, err := runCommand("psql", args...) 223 if err != nil || strings.Contains(string(result), "ERROR") { 224 if err == nil { 225 err = fmt.Errorf("\n%s", string(result)) 226 } 227 228 // If at any point we fail, log it and break 229 log.Printf("ERROR loading sql migration:%s\n", err) 230 log.Printf("All further migrations cancelled\n\n") 231 return err 232 } 233 234 migrationCount++ 235 migrations = append(migrations, filename) 236 log.Printf("Completed migration %s\n%s\n%s", filename, string(result), "-") 237 238 } 239 240 if migrationCount > 0 { 241 writeMetadata(config, migrations) 242 log.Printf("Migrations complete up to migration %v on db %s\n\n", migrations, config["db"]) 243 } 244 245 return nil 246 } 247 248 // Oh, we need to write the full list of migrations, not just one migration version 249 250 // Update the database with a line recording what we have done 251 func writeMetadata(config map[string]string, migrations []string) { 252 // Try opening the db (db may not exist at this stage) 253 err := openDatabase(config) 254 if err != nil { 255 log.Printf("Database ERROR %s", err) 256 } 257 defer query.CloseDatabase() 258 259 for _, m := range migrations { 260 sql := "Insert into fragmenta_metadata(updated_at,fragmenta_version,migration_version,status) VALUES(NOW(),$1,$2,100);" 261 result, err := query.ExecSQL(sql, fragmentaVersion, m) 262 if err != nil { 263 log.Printf("Database ERROR %s %s", err, result) 264 } 265 } 266 267 } 268 269 // Open our database 270 func openDatabase(config map[string]string) error { 271 // Open the database 272 options := map[string]string{ 273 "adapter": config["db_adapter"], 274 "user": config["db_user"], 275 "password": config["db_pass"], 276 "db": config["db"], 277 // "debug" : "true", 278 } 279 280 err := query.OpenDatabase(options) 281 if err != nil { 282 return err 283 } 284 285 log.Printf("%s\n", "-") 286 log.Printf("Opened database at %s for user %s", config["db"], config["db_user"]) 287 return nil 288 } 289 290 // Generate a suitable path for a migration from the current date/time down to nanosecond 291 func migrationPath(path string, name string) string { 292 now := time.Now() 293 layout := "2006-01-02-150405" 294 return fmt.Sprintf("%s/db/migrate/%s-%s.sql", path, now.Format(layout), name) 295 } 296 297 // Generate a random 32 byte key encoded in base64 298 func randomKey(l int64) string { 299 k := make([]byte, l) 300 if _, err := io.ReadFull(rand.Reader, k); err != nil { 301 return "" 302 } 303 return hex.EncodeToString(k) 304 } 305 306 // fileExists returns true if this file exists 307 func fileExists(p string) bool { 308 _, err := os.Stat(p) 309 if err != nil && os.IsNotExist(err) { 310 return false 311 } 312 313 return true 314 } 315 316 // runCommand runs a command with exec.Command 317 func runCommand(command string, args ...string) ([]byte, error) { 318 319 cmd := exec.Command(command, args...) 320 output, err := cmd.CombinedOutput() 321 if err != nil { 322 return output, err 323 } 324 325 return output, nil 326 }