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  }