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  }