2021-01-18 20:11:05 +03:00
package scripts
import (
"path/filepath"
"regexp"
2021-04-01 08:13:24 +03:00
"github.com/hasura/graphql-engine/cli/internal/metadataobject"
2021-03-08 14:59:35 +03:00
2021-04-01 08:13:24 +03:00
"github.com/hasura/graphql-engine/cli/internal/metadatautil"
2021-02-17 07:20:19 +03:00
2021-03-08 14:59:35 +03:00
"github.com/fatih/color"
2021-02-17 07:20:19 +03:00
"github.com/hasura/graphql-engine/cli/internal/statestore"
"github.com/hasura/graphql-engine/cli/internal/statestore/migrations"
"github.com/hasura/graphql-engine/cli/internal/statestore/settings"
2021-01-18 20:11:05 +03:00
"github.com/hasura/graphql-engine/cli"
"fmt"
"github.com/hasura/graphql-engine/cli/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/afero"
)
type UpgradeToMuUpgradeProjectToMultipleSourcesOpts struct {
EC * cli . ExecutionContext
Fs afero . Fs
// Path to project directory
ProjectDirectory string
// Directory in which migrations are stored
MigrationsAbsDirectoryPath string
SeedsAbsDirectoryPath string
Logger * logrus . Logger
}
2021-02-17 15:51:43 +03:00
// UpdateProjectV3 will help a project directory move from a single
2021-01-18 20:11:05 +03:00
// The project is expected to be in Config V2
2021-02-17 15:51:43 +03:00
func UpdateProjectV3 ( opts UpgradeToMuUpgradeProjectToMultipleSourcesOpts ) error {
2021-01-18 20:11:05 +03:00
/ * New flow
2021-02-17 07:20:19 +03:00
Config V2 - > Config V3
- Warn user about creating a backup
2021-02-17 15:51:43 +03:00
- Ask user for the name of database to migrate to
2021-02-17 07:20:19 +03:00
- copy state from hdb_tables to catalog state
- Move current migration directories to a new source directory
- Move seeds belonging to the source to a new directory
- Update config file and version
2021-01-18 20:11:05 +03:00
* /
2021-03-08 09:57:50 +03:00
// pre checks
2021-01-18 20:11:05 +03:00
if opts . EC . Config . Version != cli . V2 {
return fmt . Errorf ( "project should be using config V2 to be able to update to V3" )
}
2021-03-08 09:57:50 +03:00
if ! opts . EC . HasMetadataV3 {
return fmt . Errorf ( "unsupported server version %v, config V3 is supported only on server with metadata version >= 3" , opts . EC . Version . Server )
}
if r , err := opts . EC . APIClient . V1Metadata . GetInconsistentMetadata ( ) ; err != nil {
return fmt . Errorf ( "determing server metadata inconsistency: %w" , err )
} else {
if ! r . IsConsistent {
return fmt . Errorf ( "cannot continue: metadata is inconsistent on the server" )
}
}
opts . Logger . Infof ( "The upgrade process will make some changes to your project directory, It is advised to create a backup project directory before continuing" )
opts . Logger . Warn ( ` Config V3 is expected to be used with servers >=v2.0.0-alpha.1 ` )
opts . Logger . Warn ( ` During the update process CLI uses the server as the source of truth, so make sure your server is upto date ` )
opts . Logger . Warn ( ` The update process replaces project metadata with metadata on the server ` )
2021-01-18 20:11:05 +03:00
response , err := util . GetYesNoPrompt ( "continue?" )
if err != nil {
return err
}
if response == "n" {
return nil
}
// move migration child directories
// get directory names to move
2021-02-17 15:51:43 +03:00
targetDatabase , err := util . GetInputPrompt ( "what database does the current migrations / seeds belong to?" )
2021-01-18 20:11:05 +03:00
if err != nil {
return err
}
2021-02-17 07:20:19 +03:00
opts . EC . Spinner . Start ( )
opts . EC . Spin ( "updating project... " )
// copy state
2021-02-17 15:51:43 +03:00
// if a default database is setup copy state from it
2021-03-08 14:59:35 +03:00
sources , err := metadatautil . GetSources ( opts . EC . APIClient . V1Metadata . ExportMetadata )
2021-02-17 07:20:19 +03:00
if err != nil {
return err
}
if len ( sources ) >= 1 {
2021-02-17 15:51:43 +03:00
if err := copyState ( opts . EC , targetDatabase ) ; err != nil {
2021-02-17 07:20:19 +03:00
return err
}
}
2021-01-18 20:11:05 +03:00
// move migration child directories
// get directory names to move
migrationDirectoriesToMove , err := getMigrationDirectoryNames ( opts . Fs , opts . MigrationsAbsDirectoryPath )
if err != nil {
return errors . Wrap ( err , "getting list of migrations to move" )
}
// move seed child directories
// get directory names to move
seedFilesToMove , err := getSeedFiles ( opts . Fs , opts . SeedsAbsDirectoryPath )
if err != nil {
return errors . Wrap ( err , "getting list of seed files to move" )
}
2021-02-17 15:51:43 +03:00
// create a new directory for TargetDatabase
targetMigrationsDirectoryName := filepath . Join ( opts . MigrationsAbsDirectoryPath , targetDatabase )
2021-01-18 20:11:05 +03:00
if err = opts . Fs . Mkdir ( targetMigrationsDirectoryName , 0755 ) ; err != nil {
2021-02-17 15:51:43 +03:00
errors . Wrap ( err , "creating target migrations directory" )
2021-01-18 20:11:05 +03:00
}
2021-02-17 15:51:43 +03:00
// create a new directory for TargetDatabase
targetSeedsDirectoryName := filepath . Join ( opts . SeedsAbsDirectoryPath , targetDatabase )
2021-01-18 20:11:05 +03:00
if err = opts . Fs . Mkdir ( targetSeedsDirectoryName , 0755 ) ; err != nil {
2021-02-17 15:51:43 +03:00
errors . Wrap ( err , "creating target seeds directory" )
2021-01-18 20:11:05 +03:00
}
2021-02-17 15:51:43 +03:00
// move migration directories to target database directory
2021-01-18 20:11:05 +03:00
if err := copyMigrations ( opts . Fs , migrationDirectoriesToMove , opts . MigrationsAbsDirectoryPath , targetMigrationsDirectoryName ) ; err != nil {
2021-02-17 15:51:43 +03:00
return errors . Wrap ( err , "moving migrations to target database directory" )
2021-01-18 20:11:05 +03:00
}
2021-02-17 15:51:43 +03:00
// move seed directories to target database directory
2021-01-18 20:11:05 +03:00
if err := copyFiles ( opts . Fs , seedFilesToMove , opts . SeedsAbsDirectoryPath , targetSeedsDirectoryName ) ; err != nil {
2021-02-17 15:51:43 +03:00
return errors . Wrap ( err , "moving seeds to target database directory" )
2021-01-18 20:11:05 +03:00
}
// write new config file
newConfig := * opts . EC . Config
newConfig . Version = cli . V3
if err := opts . EC . WriteConfig ( & newConfig ) ; err != nil {
return err
}
2021-02-17 07:20:19 +03:00
opts . EC . Config = & newConfig
2021-01-18 20:11:05 +03:00
// delete original migrations
if err := removeDirectories ( opts . Fs , opts . MigrationsAbsDirectoryPath , migrationDirectoriesToMove ) ; err != nil {
return errors . Wrap ( err , "removing up original migrations" )
}
// delete original seeds
if err := removeDirectories ( opts . Fs , opts . SeedsAbsDirectoryPath , seedFilesToMove ) ; err != nil {
return errors . Wrap ( err , "removing up original migrations" )
}
2021-02-17 07:20:19 +03:00
// remove functions.yaml and tables.yaml files
metadataFiles := [ ] string { "functions.yaml" , "tables.yaml" }
if err := removeDirectories ( opts . Fs , opts . EC . MetadataDir , metadataFiles ) ; err != nil {
return err
}
2021-04-01 08:13:24 +03:00
var files map [ string ] [ ] byte
mdHandler := metadataobject . NewHandlerFromEC ( opts . EC )
files , err = mdHandler . ExportMetadata ( )
2021-02-17 07:20:19 +03:00
if err != nil {
return err
}
2021-04-01 08:13:24 +03:00
if err := mdHandler . WriteMetadata ( files ) ; err != nil {
2021-02-17 07:20:19 +03:00
return err
}
opts . EC . Spinner . Stop ( )
2021-01-18 20:11:05 +03:00
return nil
}
func removeDirectories ( fs afero . Fs , parentDirectory string , dirNames [ ] string ) error {
for _ , d := range dirNames {
if err := fs . RemoveAll ( filepath . Join ( parentDirectory , d ) ) ; err != nil {
return err
}
}
return nil
}
func copyMigrations ( fs afero . Fs , dirs [ ] string , parentDir , target string ) error {
for _ , dir := range dirs {
f , _ := fs . Stat ( filepath . Join ( parentDir , dir ) )
if f != nil {
if f . IsDir ( ) {
err := util . CopyDirAfero ( fs , filepath . Join ( parentDir , dir ) , filepath . Join ( target , dir ) )
if err != nil {
return errors . Wrapf ( err , "moving %s to %s" , dir , target )
}
2021-02-17 07:20:19 +03:00
} else {
2021-01-18 20:11:05 +03:00
err := util . CopyFileAfero ( fs , filepath . Join ( parentDir , dir ) , filepath . Join ( target , dir ) )
if err != nil {
return errors . Wrapf ( err , "moving %s to %s" , dir , target )
}
}
}
}
return nil
}
func copyFiles ( fs afero . Fs , files [ ] string , parentDir , target string ) error {
for _ , dir := range files {
err := util . CopyFileAfero ( fs , filepath . Join ( parentDir , dir ) , filepath . Join ( target , dir ) )
if err != nil {
return errors . Wrapf ( err , "moving %s to %s" , dir , target )
}
}
return nil
}
func getMigrationDirectoryNames ( fs afero . Fs , rootMigrationsDir string ) ( [ ] string , error ) {
return getMatchingFilesAndDirs ( fs , rootMigrationsDir , isHasuraCLIGeneratedMigration )
}
func getSeedFiles ( fs afero . Fs , rootSeedDir string ) ( [ ] string , error ) {
// find migrations which are in the format <timestamp>_name
var seedFiles [ ] string
dirs , err := afero . ReadDir ( fs , rootSeedDir )
if err != nil {
return nil , err
}
for _ , info := range dirs {
if ! info . IsDir ( ) {
seedFiles = append ( seedFiles , filepath . Join ( info . Name ( ) ) )
}
}
return seedFiles , nil
}
func getMatchingFilesAndDirs ( fs afero . Fs , parentDir string , matcher func ( string ) ( bool , error ) ) ( [ ] string , error ) {
// find migrations which are in the format <timestamp>_name
2021-02-17 07:20:19 +03:00
var migs [ ] string
2021-01-18 20:11:05 +03:00
dirs , err := afero . ReadDir ( fs , parentDir )
if err != nil {
return nil , err
}
for _ , info := range dirs {
if ok , err := matcher ( info . Name ( ) ) ; ! ok || err != nil {
if err != nil {
2021-02-17 07:20:19 +03:00
return nil , err
}
2021-01-18 20:11:05 +03:00
continue
}
2021-02-17 07:20:19 +03:00
migs = append ( migs , filepath . Join ( info . Name ( ) ) )
2021-01-18 20:11:05 +03:00
}
2021-02-17 07:20:19 +03:00
return migs , nil
2021-01-18 20:11:05 +03:00
}
func isHasuraCLIGeneratedMigration ( dirPath string ) ( bool , error ) {
const regex = ` ^([0-9] { 13})_(.*)$ `
return regexp . MatchString ( regex , filepath . Base ( dirPath ) )
}
2021-02-17 07:20:19 +03:00
2021-02-17 15:51:43 +03:00
func copyState ( ec * cli . ExecutionContext , destdatabase string ) error {
2021-02-17 07:20:19 +03:00
// copy migrations state
src := cli . GetMigrationsStateStore ( ec )
if err := src . PrepareMigrationsStateStore ( ) ; err != nil {
return err
}
dst := migrations . NewCatalogStateStore ( statestore . NewCLICatalogState ( ec . APIClient . V1Metadata ) )
if err := dst . PrepareMigrationsStateStore ( ) ; err != nil {
return err
}
2021-02-17 15:51:43 +03:00
err := statestore . CopyMigrationState ( src , dst , "" , destdatabase )
2021-02-17 07:20:19 +03:00
if err != nil {
return err
}
// copy settings state
srcSettingsStore := cli . GetSettingsStateStore ( ec )
if err := srcSettingsStore . PrepareSettingsDriver ( ) ; err != nil {
return err
}
dstSettingsStore := settings . NewStateStoreCatalog ( statestore . NewCLICatalogState ( ec . APIClient . V1Metadata ) )
if err := dstSettingsStore . PrepareSettingsDriver ( ) ; err != nil {
return err
}
err = statestore . CopySettingsState ( srcSettingsStore , dstSettingsStore )
if err != nil {
return err
}
return nil
}
func CheckIfUpdateToConfigV3IsRequired ( ec * cli . ExecutionContext ) error {
// see if an update to config V3 is necessary
if ec . Config . Version <= cli . V1 && ec . HasMetadataV3 {
ec . Logger . Info ( "config v1 is deprecated from v1.4" )
2021-04-01 08:13:24 +03:00
return errors . New ( "please upgrade your project to a newer version.\nuse " + color . New ( color . FgCyan ) . SprintFunc ( ) ( "hasura scripts update-project-v2" ) + " to upgrade your project to config v2" )
2021-02-17 07:20:19 +03:00
}
if ec . Config . Version < cli . V3 && ec . HasMetadataV3 {
2021-03-08 14:59:35 +03:00
sources , err := metadatautil . GetSources ( ec . APIClient . V1Metadata . ExportMetadata )
2021-02-17 07:20:19 +03:00
if err != nil {
return err
}
upgrade := func ( ) error {
2021-02-17 15:51:43 +03:00
ec . Logger . Info ( "Looks like you are trying to use hasura with multiple databases, which requires some changes on your project directory\n" )
2021-02-17 07:20:19 +03:00
ec . Logger . Info ( "please use " + color . New ( color . FgCyan ) . SprintFunc ( ) ( "hasura scripts update-project-v3" ) + " to make this change" )
return errors . New ( "update to config V3" )
}
// if no sources are configured prompt and upgrade
if len ( sources ) != 1 {
return upgrade ( )
}
2021-02-17 15:51:43 +03:00
// if 1 source is configured and it is not "default" then it's a custom database
2021-02-17 07:20:19 +03:00
// then also prompt an upgrade
if len ( sources ) == 1 {
if sources [ 0 ] != "default" {
return upgrade ( )
}
}
}
return nil
}