mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-21 02:04:09 +00:00
--app rejects a code nothing was registered under, on all three paths. It used to take a typo as "nothing matched" and report success: migrate said the app was unknown and still exited 0, while --dry-run and status printed the same words an up-to-date database produces.
252 lines
7.0 KiB
Go
252 lines
7.0 KiB
Go
package migrate
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/go-admin-team/go-admin-core/v2/sdk"
|
|
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/go-admin-team/go-admin-core/v2/config/source/file"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
|
|
"go-admin/cmd/migrate/migration"
|
|
_ "go-admin/cmd/migrate/migration/version"
|
|
_ "go-admin/cmd/migrate/migration/version-local"
|
|
"go-admin/common/database"
|
|
"go-admin/common/models"
|
|
)
|
|
|
|
var (
|
|
configYml string
|
|
generate bool
|
|
goAdmin bool
|
|
host string
|
|
appCode string
|
|
dryRun bool
|
|
StartCmd = &cobra.Command{
|
|
Use: "migrate",
|
|
Short: "Initialize the database",
|
|
Example: "go-admin migrate -c config/settings.yml",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
run()
|
|
},
|
|
}
|
|
statusCmd = &cobra.Command{
|
|
Use: "status",
|
|
Short: "List applied and pending migrations, grouped by app",
|
|
Example: "go-admin migrate status -c config/settings.yml",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
runStatus()
|
|
},
|
|
}
|
|
)
|
|
|
|
// fixme 在您看不见代码的时候运行迁移,我觉得是不安全的,所以编译后最好不要去执行迁移
|
|
func init() {
|
|
StartCmd.PersistentFlags().StringVarP(&configYml, "config", "c", "config/settings.yml", "Start server with provided configuration file")
|
|
StartCmd.PersistentFlags().BoolVarP(&generate, "generate", "g", false, "generate migration file")
|
|
StartCmd.PersistentFlags().BoolVarP(&goAdmin, "goAdmin", "a", false, "with -g, write the generated file to version/ instead of version-local/ (does not affect which migrations run)")
|
|
StartCmd.PersistentFlags().StringVarP(&host, "domain", "d", "*", "select tenant host")
|
|
|
|
// --app is deliberately long-only. -a already means "generate into
|
|
// version/ rather than version-local/", which is about writing a template
|
|
// file, not about which migrations run; giving the two the same letter
|
|
// would be a trap.
|
|
StartCmd.PersistentFlags().StringVar(&appCode, "app", "", "limit to the migrations of one app (\""+migration.FrameworkAppCode+"\" for the framework's own)")
|
|
StartCmd.Flags().BoolVar(&dryRun, "dry-run", false, "list what would be applied, in order, and write nothing")
|
|
|
|
StartCmd.AddCommand(statusCmd)
|
|
}
|
|
|
|
func run() {
|
|
|
|
if !generate {
|
|
fmt.Println(`start init`)
|
|
//1. 读取配置
|
|
config.Setup(
|
|
file.NewSource(file.WithPath(configYml)),
|
|
initDB,
|
|
)
|
|
} else {
|
|
fmt.Println(`generate migration file`)
|
|
_ = genFile()
|
|
}
|
|
}
|
|
|
|
// resolveDB picks the tenant database and hands it to the registry.
|
|
//
|
|
// It creates and alters nothing, which is what lets status and --dry-run share
|
|
// it: those two must be able to run against a production database without
|
|
// leaving a trace.
|
|
func resolveDB() (*gorm.DB, error) {
|
|
if host == "" {
|
|
host = "*"
|
|
}
|
|
db := sdk.Runtime.GetDbByTenant(host)
|
|
if db == nil {
|
|
if len(sdk.Runtime.GetAllDb()) == 1 && host == "*" {
|
|
for k, v := range sdk.Runtime.GetAllDb() {
|
|
db = v
|
|
host = k
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if db == nil {
|
|
return nil, fmt.Errorf("未找到数据库配置")
|
|
}
|
|
if config.DatabasesConfig[host].Driver == "mysql" {
|
|
//初始化数据库时候用
|
|
db.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4")
|
|
}
|
|
return db, nil
|
|
}
|
|
|
|
// exitUnlessAppRegistered ends the command when --app names something no
|
|
// migration was registered under.
|
|
//
|
|
// Every path took a typo as "nothing matched" and reported success: `migrate`
|
|
// printed that the app was unknown and still exited 0, while `--dry-run` and
|
|
// `status` said "nothing to apply" and "none recorded" - which is what an
|
|
// up-to-date database says too, so the output does not even hint at the typo.
|
|
// An operator running `go-admin migrate --app crmm && deploy` gets the deploy.
|
|
//
|
|
// Checked against the registry, which init() has already filled, so this runs
|
|
// before any database work and costs nothing. It lives in the command layer
|
|
// because the exit code does: the migration package stays callable from a test
|
|
// without taking the process down with it.
|
|
func exitUnlessAppRegistered() {
|
|
if err := appRegistrationError(); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// appRegistrationError carries the decision on its own so it can be tested;
|
|
// exitUnlessAppRegistered is only the os.Exit around it. Nil means --app was
|
|
// either empty or names a registered app.
|
|
func appRegistrationError() error {
|
|
if appCode == "" {
|
|
return nil
|
|
}
|
|
want := migration.DisplayAppCode(migration.AppFilter(appCode))
|
|
registered := migration.Migrate.AppCodes()
|
|
for _, c := range registered {
|
|
if c == want {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("no migrations are registered for app %q; registered: %s",
|
|
want, strings.Join(registered, ", "))
|
|
}
|
|
|
|
func migrateModel() error {
|
|
db, err := resolveDB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// sys_migration is the one table that never goes through a versioned
|
|
// migration - it is the table that records them. AutoMigrate realigns it
|
|
// on every run, which is how the app_code column reaches an existing
|
|
// database without anyone writing a migration for it.
|
|
if err = db.Debug().AutoMigrate(&models.Migration{}); err != nil {
|
|
return err
|
|
}
|
|
migration.Migrate.SetDb(db.Debug())
|
|
if appCode != "" {
|
|
migration.Migrate.MigrateApp(appCode)
|
|
return nil
|
|
}
|
|
migration.Migrate.Migrate()
|
|
return nil
|
|
}
|
|
|
|
func initDB() {
|
|
// Before the database is touched, so a typo cannot get as far as looking
|
|
// like a successful no-op on either path below.
|
|
exitUnlessAppRegistered()
|
|
|
|
//3. 初始化数据库链接
|
|
database.Setup()
|
|
|
|
if dryRun {
|
|
db, err := resolveDB()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
migration.Migrate.SetDb(db)
|
|
entries, err := migration.Migrate.Status()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
if err = printPending(os.Stdout, entries, appCode); err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
//4. 数据库迁移
|
|
fmt.Println("数据库迁移开始")
|
|
if err := migrateModel(); err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
fmt.Println(`数据库基础数据初始化成功`)
|
|
}
|
|
|
|
func runStatus() {
|
|
config.Setup(
|
|
file.NewSource(file.WithPath(configYml)),
|
|
func() {
|
|
exitUnlessAppRegistered()
|
|
|
|
database.Setup()
|
|
db, err := resolveDB()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
migration.Migrate.SetDb(db)
|
|
entries, err := migration.Migrate.Status()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
if err = printStatus(os.Stdout, entries, appCode); err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
func genFile() error {
|
|
t1, err := template.ParseFiles("template/migrate.template")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m := map[string]string{}
|
|
m["GenerateTime"] = strconv.FormatInt(time.Now().UnixNano()/1e6, 10)
|
|
m["Package"] = "version_local"
|
|
if goAdmin {
|
|
m["Package"] = "version"
|
|
}
|
|
var b1 bytes.Buffer
|
|
err = t1.Execute(&b1, m)
|
|
if goAdmin {
|
|
pkg.FileCreate(b1, "./cmd/migrate/migration/version/"+m["GenerateTime"]+"_migrate.go")
|
|
} else {
|
|
pkg.FileCreate(b1, "./cmd/migrate/migration/version-local/"+m["GenerateTime"]+"_migrate.go")
|
|
}
|
|
return nil
|
|
}
|