2021-01-18 20:11:05 +03:00
package scripts
import (
"path/filepath"
"regexp"
2021-07-23 12:49:44 +03:00
"github.com/hasura/graphql-engine/cli/v2/internal/projectmetadata"
2021-03-08 14:59:35 +03:00
2021-06-16 14:44:15 +03:00
"github.com/hasura/graphql-engine/cli/v2/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
2021-06-16 14:44:15 +03:00
"github.com/hasura/graphql-engine/cli/v2/internal/statestore"
"github.com/hasura/graphql-engine/cli/v2/internal/statestore/migrations"
"github.com/hasura/graphql-engine/cli/v2/internal/statestore/settings"
2021-02-17 07:20:19 +03:00
2021-06-16 14:44:15 +03:00
"github.com/hasura/graphql-engine/cli/v2"
2021-01-18 20:11:05 +03:00
"fmt"
2021-06-16 14:44:15 +03:00
"github.com/hasura/graphql-engine/cli/v2/util"
2021-01-18 20:11:05 +03:00
"github.com/sirupsen/logrus"
"github.com/spf13/afero"
)
2021-05-17 18:19:15 +03:00
type UpdateProjectV3Opts struct {
2021-01-18 20:11:05 +03:00
EC * cli . ExecutionContext
Fs afero . Fs
// Path to project directory
ProjectDirectory string
// Directory in which migrations are stored
MigrationsAbsDirectoryPath string
SeedsAbsDirectoryPath string
2021-05-17 18:19:15 +03:00
TargetDatabase string
Force bool
MoveStateOnly bool
2021-01-18 20:11:05 +03:00
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-05-17 18:19:15 +03:00
func UpdateProjectV3 ( opts UpdateProjectV3Opts ) 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-05-17 18:19:15 +03:00
if opts . EC . Config . Version != cli . V2 && ! opts . MoveStateOnly {
2021-01-18 20:11:05 +03:00
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 {
2021-10-08 19:09:30 +03:00
return fmt . Errorf ( "cannot upgrade: unsupported server version %v, config V3 is supported only on server with metadata version >= 3" , opts . EC . Version . Server )
2021-03-08 09:57:50 +03:00
}
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-10-08 19:09:30 +03:00
opts . Logger . Infof ( "Using %s server at %s for update" , opts . EC . Version . GetServerVersion ( ) , opts . EC . Config . ServerConfig . Endpoint )
2021-01-18 20:11:05 +03:00
2021-10-13 17:38:07 +03:00
if ! opts . Force && opts . EC . IsTerminal {
2021-05-17 18:19:15 +03:00
response , err := util . GetYesNoPrompt ( "continue?" )
if err != nil {
return err
}
2021-09-07 16:33:58 +03:00
if ! response {
2021-05-17 18:19:15 +03:00
return nil
}
2021-01-18 20:11:05 +03:00
}
2021-05-28 09:04:36 +03:00
// if database name is set using --database-name flag, copy it to this variable
2021-05-17 18:19:15 +03:00
targetDatabase := opts . TargetDatabase
2021-05-28 09:04:36 +03:00
// if targetDatabase is not set, get list of databases connected from hasura
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
}
2021-05-28 09:04:36 +03:00
if len ( targetDatabase ) == 0 {
if len ( sources ) == 1 && sources [ 0 ] == "default" {
targetDatabase = sources [ 0 ]
} else if len ( sources ) > 0 {
targetDatabase , err = util . GetSelectPrompt ( "what database does this current migrations / seeds belong to?" , sources )
if err != nil {
return err
}
} else {
return fmt . Errorf ( "cannot determine name of database for which current migrations / seed belong to, found 0 connected databases on hasura %v" , sources )
}
}
opts . EC . Spinner . Start ( )
opts . EC . Spin ( "updating project... " )
defer opts . EC . Spinner . Stop ( )
2021-02-17 07:20:19 +03:00
if len ( sources ) >= 1 {
2021-05-28 09:04:36 +03:00
opts . EC . Logger . Debug ( "start: copying state from from hdb_catalog.schema_migrations" )
opts . EC . Spin ( "Moving state from hdb_catalog.schema_migrations " )
if err := CopyState ( opts . EC , targetDatabase , targetDatabase ) ; err != nil {
2021-02-17 07:20:19 +03:00
return err
}
2021-05-17 18:19:15 +03:00
if opts . MoveStateOnly {
2021-05-28 09:04:36 +03:00
opts . EC . Logger . Debug ( "move state only is set, copied state and returning early" )
2021-05-17 18:19:15 +03:00
return nil
}
2021-05-28 09:04:36 +03:00
opts . EC . Logger . Debug ( "completed: copying state from from hdb_catalog.schema_migrations" )
2021-02-17 07:20:19 +03:00
}
2021-05-28 09:04:36 +03:00
opts . EC . Logger . Debug ( "start: copy old migrations to new directory structure" )
opts . EC . Spin ( "Moving migrations and seeds to new directories " )
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 {
2021-10-13 17:38:07 +03:00
return fmt . Errorf ( "getting list of migrations to move: %w" , err )
2021-01-18 20:11:05 +03:00
}
// move seed child directories
// get directory names to move
seedFilesToMove , err := getSeedFiles ( opts . Fs , opts . SeedsAbsDirectoryPath )
if err != nil {
2021-10-13 17:38:07 +03:00
return fmt . Errorf ( "getting list of seed files to move: %w" , err )
2021-01-18 20:11:05 +03:00
}
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-10-13 17:38:07 +03:00
return fmt . Errorf ( "creating target migrations directory: %w" , err )
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-10-13 17:38:07 +03:00
return fmt . Errorf ( "creating target seeds directory: %w" , err )
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-10-13 17:38:07 +03:00
return fmt . Errorf ( "moving migrations to target database directory: %w" , err )
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-10-13 17:38:07 +03:00
return fmt . Errorf ( "moving seeds to target database directory: %w" , err )
2021-01-18 20:11:05 +03:00
}
2021-05-28 09:04:36 +03:00
opts . EC . Logger . Debug ( "completed: copy old migrations to new directory structure" )
2021-01-18 20:11:05 +03:00
2021-05-28 09:04:36 +03:00
opts . EC . Logger . Debug ( "start: generate new config file" )
opts . EC . Spin ( "Generating new config file " )
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-05-28 09:04:36 +03:00
opts . EC . Logger . Debug ( "completed: generate new config file" )
2021-01-18 20:11:05 +03:00
2021-05-28 09:04:36 +03:00
opts . EC . Logger . Debug ( "start: delete old migrations and seeds" )
opts . EC . Spin ( "Cleaning project directory " )
2021-01-18 20:11:05 +03:00
// delete original migrations
if err := removeDirectories ( opts . Fs , opts . MigrationsAbsDirectoryPath , migrationDirectoriesToMove ) ; err != nil {
2021-10-13 17:38:07 +03:00
return fmt . Errorf ( "removing up original migrations: %w" , err )
2021-01-18 20:11:05 +03:00
}
// delete original seeds
if err := removeDirectories ( opts . Fs , opts . SeedsAbsDirectoryPath , seedFilesToMove ) ; err != nil {
2021-10-13 17:38:07 +03:00
return fmt . Errorf ( "removing up original migrations: %w" , err )
2021-01-18 20:11:05 +03:00
}
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-05-28 09:04:36 +03:00
opts . EC . Logger . Debug ( "completed: delete old migrations and seeds" )
opts . EC . Logger . Debug ( "start: export metadata from server" )
opts . EC . Spin ( "Exporting metadata from server " )
2021-04-01 08:13:24 +03:00
var files map [ string ] [ ] byte
2021-07-23 12:49:44 +03:00
mdHandler := projectmetadata . NewHandlerFromEC ( opts . EC )
2021-04-01 08:13:24 +03:00
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-05-28 09:04:36 +03:00
opts . EC . Logger . Debug ( "completed: export metadata from server" )
opts . EC . Logger . Info ( "Operation completed" )
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 {
2021-10-13 17:38:07 +03:00
return fmt . Errorf ( "moving %s to %s : %w" , dir , target , err )
2021-01-18 20:11:05 +03:00
}
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 {
2021-10-13 17:38:07 +03:00
return fmt . Errorf ( "moving %s to %s : %w" , dir , target , err )
2021-01-18 20:11:05 +03:00
}
}
}
}
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 {
2021-10-13 17:38:07 +03:00
return fmt . Errorf ( "moving %s to %s : %w" , dir , target , err )
2021-01-18 20:11:05 +03:00
}
}
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-05-28 09:04:36 +03:00
func CopyState ( ec * cli . ExecutionContext , sourceDatabase , destDatabase string ) error {
2021-02-17 07:20:19 +03:00
// copy migrations state
2021-05-17 18:19:15 +03:00
src := migrations . NewMigrationStateStoreHdbTable ( ec . APIClient . V2Query , migrations . DefaultSchema , migrations . DefaultMigrationsTable )
2021-05-28 09:04:36 +03:00
if err := src . PrepareMigrationsStateStore ( sourceDatabase ) ; err != nil {
2021-02-17 07:20:19 +03:00
return err
}
dst := migrations . NewCatalogStateStore ( statestore . NewCLICatalogState ( ec . APIClient . V1Metadata ) )
2021-05-28 09:04:36 +03:00
if err := dst . PrepareMigrationsStateStore ( destDatabase ) ; err != nil {
2021-02-17 07:20:19 +03:00
return err
}
2021-05-28 09:04:36 +03:00
err := statestore . CopyMigrationState ( src , dst , sourceDatabase , destDatabase )
2021-02-17 07:20:19 +03:00
if err != nil {
return err
}
// copy settings state
2021-05-28 09:04:36 +03:00
srcSettingsStore := cli . GetSettingsStateStore ( ec , sourceDatabase )
2021-02-17 07:20:19 +03:00
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
}
2021-05-17 18:19:15 +03:00
cliState , err := statestore . NewCLICatalogState ( ec . APIClient . V1Metadata ) . Get ( )
if err != nil {
2021-10-13 17:38:07 +03:00
return fmt . Errorf ( "error while fetching catalog state: %w" , err )
2021-05-17 18:19:15 +03:00
}
cliState . IsStateCopyCompleted = true
if _ , err := statestore . NewCLICatalogState ( ec . APIClient . V1Metadata ) . Set ( * cliState ) ; err != nil {
2021-10-13 17:38:07 +03:00
return fmt . Errorf ( "cannot set catalog state: %w" , err )
2021-05-17 18:19:15 +03:00
}
2021-02-17 07:20:19 +03:00
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-10-13 17:38:07 +03:00
return fmt . Errorf ( "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" )
2021-10-13 17:38:07 +03:00
return fmt . Errorf ( "update to config V3" )
2021-02-17 07:20:19 +03:00
}
2021-05-28 09:04:36 +03:00
if len ( sources ) == 0 {
return fmt . Errorf ( "no connected databases found on hasura" )
}
2021-02-17 07:20:19 +03:00
// 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
}