2018-06-24 16:40:48 +03:00
package commands
import (
2022-11-21 15:07:28 +03:00
"errors"
2021-03-08 14:59:35 +03:00
"fmt"
2018-09-10 16:21:30 +03:00
"os"
2021-04-01 15:58:24 +03:00
"path/filepath"
2018-06-24 16:40:48 +03:00
"strconv"
2021-09-23 09:49:32 +03:00
"strings"
2018-06-24 16:40:48 +03:00
2022-11-21 15:07:28 +03:00
"github.com/hasura/graphql-engine/cli/v2"
herrors "github.com/hasura/graphql-engine/cli/v2/internal/errors"
2021-06-18 09:24:16 +03:00
"github.com/hasura/graphql-engine/cli/v2/internal/hasura"
2021-06-16 14:44:15 +03:00
"github.com/hasura/graphql-engine/cli/v2/internal/metadatautil"
migrate "github.com/hasura/graphql-engine/cli/v2/migrate"
2018-06-24 16:40:48 +03:00
"github.com/spf13/cobra"
)
2018-06-28 11:36:25 +03:00
func newMigrateApplyCmd ( ec * cli . ExecutionContext ) * cobra . Command {
2020-02-24 19:14:46 +03:00
opts := & MigrateApplyOptions {
2018-06-24 16:40:48 +03:00
EC : ec ,
}
migrateApplyCmd := & cobra . Command {
2020-02-03 10:03:32 +03:00
Use : "apply" ,
Short : "Apply migrations on the database" ,
2023-01-09 07:25:53 +03:00
Long : ` Migrations represent the modifications needed to reach the desired state of a database schema . Running this command will apply the migrations on the database .
Further reading :
- https : //hasura.io/docs/latest/migrations-metadata-seeds/manage-migrations/
` ,
2019-12-12 08:16:36 +03:00
Example : ` # Apply all migrations
hasura migrate apply
# Use with admin secret :
hasura migrate apply -- admin - secret "<admin-secret>"
# Apply migrations on another Hasura instance :
hasura migrate apply -- endpoint "<endpoint>"
# Mark migration as applied on the server and skip execution :
2021-06-21 18:56:50 +03:00
hasura migrate apply -- skip - execution -- version "<version>"
# Mark migrations as applied on the server and skip execution :
hasura migrate apply -- skip - execution -- up all
# Mark migrations as rollbacked on the server and skip execution :
hasura migrate apply -- skip - execution -- down all
2019-12-12 08:16:36 +03:00
# Apply a particular migration version only :
hasura migrate apply -- version "<version>"
# Apply last 2 down migrations :
hasura migrate apply -- down 2
# Apply only 2 up migrations :
hasura migrate apply -- up 2
# Apply only a particular version
hasura migrate apply -- type up -- version "<version>"
2020-02-03 10:03:32 +03:00
# Apply all up migrations upto version 125 , last applied is 100
hasura migrate apply -- goto 125
# Apply all down migrations upto version 125 , last applied is 150
hasura migrate apply -- goto 125
2019-12-12 08:16:36 +03:00
# Rollback a particular version :
hasura migrate apply -- type down -- version "<version>"
# Rollback all migrations :
hasura migrate apply -- down all ` ,
2018-06-24 16:40:48 +03:00
SilenceUsage : true ,
2018-09-27 16:57:17 +03:00
PreRunE : func ( cmd * cobra . Command , args [ ] string ) error {
2022-11-21 15:07:28 +03:00
op := genOpName ( cmd , "PreRunE" )
if err := validateConfigV3FlagsWithAll ( cmd , ec ) ; err != nil {
return herrors . E ( op , err )
}
return nil
2018-09-27 16:57:17 +03:00
} ,
2018-06-24 16:40:48 +03:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2022-11-21 15:07:28 +03:00
op := genOpName ( cmd , "RunE" )
if err := opts . Run ( ) ; err != nil {
return herrors . E ( op , err )
}
return nil
2018-06-24 16:40:48 +03:00
} ,
}
f := migrateApplyCmd . Flags ( )
2020-02-03 10:03:32 +03:00
f . SortFlags = false
2018-06-24 16:40:48 +03:00
2020-02-24 19:14:46 +03:00
f . StringVar ( & opts . UpMigration , "up" , "" , "apply all or N up migration steps" )
f . StringVar ( & opts . DownMigration , "down" , "" , "apply all or N down migration steps" )
f . StringVar ( & opts . GotoVersion , "goto" , "" , "apply migration chain up to to the version specified" )
2018-09-27 16:57:17 +03:00
2020-02-24 19:14:46 +03:00
f . StringVar ( & opts . VersionMigration , "version" , "" , "only apply this particular migration" )
f . BoolVar ( & opts . SkipExecution , "skip-execution" , false , "skip executing the migration action, but mark them as applied" )
f . StringVar ( & opts . MigrationType , "type" , "up" , "type of migration (up, down) to be used with version flag" )
2018-09-27 16:57:17 +03:00
2021-06-18 09:24:16 +03:00
f . BoolVar ( & opts . DryRun , "dry-run" , false , "print the names of migrations which are going to be applied" )
2021-12-23 18:58:53 +03:00
f . BoolVar ( & opts . EC . AllDatabases , "all-databases" , false , "set this flag to attempt to apply migrations on all databases present on server" )
2021-09-14 17:27:50 +03:00
f . BoolVar ( & opts . ProgressBarLogs , "progressbar-logs" , false , "print the logs of progressbar" )
2021-10-13 17:38:07 +03:00
if err := f . MarkHidden ( "progressbar-logs" ) ; err != nil {
ec . Logger . WithError ( err ) . Errorf ( "error while using a dependency library" )
}
2018-06-24 16:40:48 +03:00
return migrateApplyCmd
}
2020-02-24 19:14:46 +03:00
type MigrateApplyOptions struct {
2018-06-24 16:40:48 +03:00
EC * cli . ExecutionContext
2020-02-24 19:14:46 +03:00
UpMigration string
DownMigration string
VersionMigration string
MigrationType string
2020-02-03 10:03:32 +03:00
// version up to which migration chain has to be applied
2021-09-14 17:27:50 +03:00
GotoVersion string
SkipExecution bool
DryRun bool
Source cli . Source
ProgressBarLogs bool
2021-06-18 09:24:16 +03:00
}
func ( o * MigrateApplyOptions ) Validate ( ) error {
2022-11-21 15:07:28 +03:00
var op herrors . Op = "commands.MigrateApplyOptions.Validate"
2021-06-18 09:24:16 +03:00
if o . EC . Config . Version == cli . V2 {
o . Source . Kind = hasura . SourceKindPG
o . Source . Name = ""
}
2021-08-16 09:43:11 +03:00
if o . DryRun && o . SkipExecution {
2022-11-21 15:07:28 +03:00
return herrors . E ( op , "both --skip-execution and --dry-run flags cannot be used together" )
2021-08-16 09:43:11 +03:00
}
2021-12-23 18:58:53 +03:00
if o . DryRun && o . EC . AllDatabases {
2022-11-21 15:07:28 +03:00
return herrors . E ( op , "both --all-databases and --dry-run flags cannot be used together" )
2021-08-16 09:43:11 +03:00
}
2021-06-18 09:24:16 +03:00
if o . EC . Config . Version >= cli . V3 {
2021-12-23 18:58:53 +03:00
if ! o . EC . AllDatabases && len ( o . Source . Name ) == 0 {
2022-11-21 15:07:28 +03:00
return herrors . E ( op , "unable to determine database on which migration should be applied" )
2021-06-18 09:24:16 +03:00
}
2021-12-23 18:58:53 +03:00
if ! o . EC . AllDatabases {
2021-06-18 09:24:16 +03:00
if len ( o . Source . Name ) == 0 {
2022-11-21 15:07:28 +03:00
return herrors . E ( op , "empty database name" )
2021-06-18 09:24:16 +03:00
}
if len ( o . Source . Kind ) == 0 {
// find out the database kind by making a API call to server
// and update ec to include the database name and kind
sourceKind , err := metadatautil . GetSourceKind ( o . EC . APIClient . V1Metadata . ExportMetadata , o . Source . Name )
if err != nil {
2023-01-09 11:01:49 +03:00
return herrors . E ( op , fmt . Errorf ( "determining database kind of '%s': %w" , o . Source . Name , err ) )
2021-06-18 09:24:16 +03:00
}
if sourceKind == nil {
2023-01-09 11:01:49 +03:00
return herrors . E ( op , fmt . Errorf ( "error determining database kind for '%s', check if database exists on hasura" , o . Source . Name ) )
2021-06-18 09:24:16 +03:00
}
o . Source . Kind = * sourceKind
}
}
}
return nil
2021-04-01 15:58:24 +03:00
}
2021-06-18 09:24:16 +03:00
2021-04-01 15:58:24 +03:00
type errDatabaseMigrationDirectoryNotFound struct {
message string
2018-06-24 16:40:48 +03:00
}
2021-04-01 15:58:24 +03:00
func ( e * errDatabaseMigrationDirectoryNotFound ) Error ( ) string {
return e . message
}
2021-08-16 09:43:11 +03:00
2020-02-24 19:14:46 +03:00
func ( o * MigrateApplyOptions ) Run ( ) error {
2022-11-21 15:07:28 +03:00
var op herrors . Op = "commands.MigrateApplyOptions.Run"
2021-08-16 09:43:11 +03:00
results , err := o . Apply ( )
if err != nil {
2022-11-21 15:07:28 +03:00
return herrors . E ( op , err )
2021-08-16 09:43:11 +03:00
}
2021-09-23 09:49:32 +03:00
var failedSources [ ] string
2021-08-16 09:43:11 +03:00
for result := range results {
if result . Error != nil {
2021-09-23 09:49:32 +03:00
failedSources = append ( failedSources , result . DatabaseName )
2021-08-16 09:43:11 +03:00
o . EC . Logger . Errorf ( "%v" , result . Error )
} else if len ( result . Message ) > 0 {
o . EC . Logger . Infof ( result . Message )
}
}
2021-09-23 09:49:32 +03:00
if len ( failedSources ) != 0 {
2023-01-09 11:01:49 +03:00
return herrors . E ( op , fmt . Errorf ( "applying migrations failed on database(s): %s" , strings . Join ( failedSources , "," ) ) )
2021-09-23 09:49:32 +03:00
}
2021-08-16 09:43:11 +03:00
return nil
}
type MigrateApplyResult struct {
DatabaseName string
Message string
Error error
}
func ( o * MigrateApplyOptions ) Apply ( ) ( chan MigrateApplyResult , error ) {
2022-11-21 15:07:28 +03:00
var op herrors . Op = "commands.MigrateApplyOptions.Apply"
2021-08-16 09:43:11 +03:00
resultChan := make ( chan MigrateApplyResult )
handleError := func ( err error ) ( string , error ) {
2022-11-21 15:07:28 +03:00
var op herrors . Op = "commands.MigrateApplyOptions.Apply.handleError"
2022-02-08 17:29:34 +03:00
if err == nil {
return "" , nil
}
var errPath * os . PathError
var errNotFound * errDatabaseMigrationDirectoryNotFound
switch {
case errors . Is ( err , migrate . ErrNoChange ) :
2023-01-09 11:01:49 +03:00
return fmt . Sprintf ( "nothing to apply on database: %s" , o . Source . Name ) , nil
2022-02-08 17:29:34 +03:00
case errors . As ( err , & errPath ) :
2021-08-16 09:43:11 +03:00
// If Op is first, then log No migrations to apply
2022-02-08 17:29:34 +03:00
if errPath . Op == "first" {
2023-01-09 11:01:49 +03:00
return fmt . Sprintf ( "nothing to apply on database: %s" , o . Source . Name ) , nil
2021-08-16 09:43:11 +03:00
}
2022-02-08 17:29:34 +03:00
case errors . As ( err , & errNotFound ) :
2021-08-16 09:43:11 +03:00
// check if the returned error is a directory not found error
// ie might be because a migrations/<source_name> directory is not found
// if so skip this
2023-01-09 11:01:49 +03:00
return "" , herrors . E ( op , fmt . Errorf ( "skipping applying migrations on database '%s', encountered: \n%s" , o . Source . Name , errNotFound . Error ( ) ) )
2021-08-16 09:43:11 +03:00
}
2023-01-09 11:01:49 +03:00
return "" , herrors . E ( op , fmt . Errorf ( "skipping applying migrations on database '%s', encountered: \n%w" , o . Source . Name , err ) )
2021-08-16 09:43:11 +03:00
}
2021-12-23 18:58:53 +03:00
if len ( o . Source . Name ) == 0 && ! o . EC . AllDatabases {
2021-06-18 09:24:16 +03:00
o . Source = o . EC . Source
}
if err := o . Validate ( ) ; err != nil {
2022-11-21 15:07:28 +03:00
return nil , herrors . E ( op , err )
2021-06-18 09:24:16 +03:00
}
2021-12-23 18:58:53 +03:00
if o . EC . AllDatabases && o . EC . Config . Version >= cli . V3 {
2021-06-18 09:24:16 +03:00
o . EC . Spin ( "getting lists of databases from server " )
sourcesAndKind , err := metadatautil . GetSourcesAndKind ( o . EC . APIClient . V1Metadata . ExportMetadata )
o . EC . Spinner . Stop ( )
if err != nil {
2022-11-21 15:07:28 +03:00
return nil , herrors . E ( op , err )
2021-06-18 09:24:16 +03:00
}
2021-08-16 09:43:11 +03:00
go func ( ) {
defer close ( resultChan )
for _ , source := range sourcesAndKind {
result := MigrateApplyResult {
DatabaseName : source . Name ,
Message : "" ,
Error : nil ,
2021-06-18 09:24:16 +03:00
}
2021-08-16 09:43:11 +03:00
o . Source . Kind = source . Kind
o . Source . Name = source . Name
err := o . Exec ( )
if err != nil {
result . Message , result . Error = handleError ( err )
} else {
result . Message = fmt . Sprintf ( "migrations applied on database: %s" , o . Source . Name )
2021-06-18 09:24:16 +03:00
}
2021-08-16 09:43:11 +03:00
resultChan <- result
2021-06-18 09:24:16 +03:00
}
2021-08-16 09:43:11 +03:00
} ( )
2021-06-18 09:24:16 +03:00
} else {
2021-08-16 09:43:11 +03:00
go func ( ) {
defer close ( resultChan )
result := MigrateApplyResult {
DatabaseName : o . Source . Name ,
Message : "" ,
Error : nil ,
2021-06-18 09:24:16 +03:00
}
2021-08-16 09:43:11 +03:00
err := o . Exec ( )
if err != nil {
result . Message , result . Error = handleError ( err )
} else {
result . Message = "migrations applied"
2021-06-18 09:24:16 +03:00
}
2021-08-16 09:43:11 +03:00
resultChan <- result
} ( )
2021-06-18 09:24:16 +03:00
}
2021-08-16 09:43:11 +03:00
return resultChan , nil
2021-06-18 09:24:16 +03:00
}
2021-08-16 09:43:11 +03:00
2021-06-18 09:24:16 +03:00
func ( o * MigrateApplyOptions ) Exec ( ) error {
2022-11-21 15:07:28 +03:00
var op herrors . Op = "commands.MigrateApplyOptions.Exec"
2021-04-01 15:58:24 +03:00
if o . EC . Config . Version >= cli . V3 {
// check if a migrations directory exists for source in project
migrationDirectory := filepath . Join ( o . EC . MigrationDir , o . Source . Name )
if f , err := os . Stat ( migrationDirectory ) ; err != nil || f == nil {
2023-01-09 11:01:49 +03:00
return herrors . E ( op , & errDatabaseMigrationDirectoryNotFound { fmt . Sprintf ( "expected to find a migrations directory for database '%s' in %s, but encountered error: %s" , o . Source . Name , o . EC . MigrationDir , err . Error ( ) ) } )
2021-04-01 15:58:24 +03:00
}
}
2021-12-23 18:58:53 +03:00
if o . EC . AllDatabases && ( len ( o . GotoVersion ) > 0 || len ( o . VersionMigration ) > 0 ) {
2022-11-21 15:07:28 +03:00
return herrors . E ( op , "cannot use --goto or --version in conjunction with --all-databases" )
2021-04-01 15:58:24 +03:00
}
2020-02-24 19:14:46 +03:00
migrationType , step , err := getMigrationTypeAndStep ( o . UpMigration , o . DownMigration , o . VersionMigration , o . MigrationType , o . GotoVersion , o . SkipExecution )
2018-06-24 16:40:48 +03:00
if err != nil {
2022-11-21 15:07:28 +03:00
return herrors . E ( op , fmt . Errorf ( "error validating flags: %w" , err ) )
2018-06-24 16:40:48 +03:00
}
2021-03-08 14:59:35 +03:00
migrateDrv , err := migrate . NewMigrate ( o . EC , true , o . Source . Name , o . Source . Kind )
2018-06-24 16:40:48 +03:00
if err != nil {
2022-11-21 15:07:28 +03:00
return herrors . E ( op , err )
2018-06-24 16:40:48 +03:00
}
2020-02-24 19:14:46 +03:00
migrateDrv . SkipExecution = o . SkipExecution
2021-06-18 09:24:16 +03:00
migrateDrv . DryRun = o . DryRun
2021-09-14 17:27:50 +03:00
migrateDrv . ProgressBarLogs = o . ProgressBarLogs
2018-06-24 16:40:48 +03:00
2022-11-21 15:07:28 +03:00
if err := ExecuteMigration ( migrationType , migrateDrv , step ) ; err != nil {
return herrors . E ( op , err )
}
return nil
2018-06-24 16:40:48 +03:00
}
2018-06-28 11:36:25 +03:00
// Only one flag out of up, down and version can be set at a time. This function
// checks whether that is the case and returns an error is not
2020-02-03 10:03:32 +03:00
func getMigrationTypeAndStep ( upMigration , downMigration , versionMigration , migrationType , gotoVersion string , skipExecution bool ) ( string , int64 , error ) {
2022-11-21 15:07:28 +03:00
var op herrors . Op = "commands.getMigrationTypeAndStep"
2018-06-24 16:40:48 +03:00
var flagCount = 0
var stepString = "all"
var migrationName = "up"
2018-06-28 11:36:25 +03:00
if upMigration != "" {
2018-06-24 16:40:48 +03:00
stepString = upMigration
flagCount ++
}
2018-06-28 11:36:25 +03:00
if downMigration != "" {
2018-06-24 16:40:48 +03:00
migrationName = "down"
stepString = downMigration
flagCount ++
}
2018-06-28 11:36:25 +03:00
if versionMigration != "" {
2018-06-24 16:40:48 +03:00
migrationName = "version"
stepString = versionMigration
2018-06-28 11:36:25 +03:00
if migrationType == "down" {
2018-06-24 16:40:48 +03:00
stepString = "-" + stepString
}
flagCount ++
}
2020-02-03 10:03:32 +03:00
if gotoVersion != "" {
migrationName = "gotoVersion"
stepString = gotoVersion
flagCount ++
}
2018-06-24 16:40:48 +03:00
if flagCount > 1 {
2022-11-21 15:07:28 +03:00
return "" , 0 , herrors . E ( op , "only one migration type can be applied at a time (--up, --down, --type or --goto)" )
2018-06-24 16:40:48 +03:00
}
2021-06-21 18:56:50 +03:00
skipExecutionValid := migrationName == "version" || migrationName == "up" || migrationName == "down"
if ! skipExecutionValid && skipExecution {
2022-11-21 15:07:28 +03:00
return "" , 0 , herrors . E ( op , "--skip-execution flag can be set only with --version, --up, --down flags" )
2019-03-18 19:40:04 +03:00
}
2018-06-24 16:40:48 +03:00
if stepString == "all" && migrationName != "version" {
return migrationName , - 1 , nil
}
step , err := strconv . ParseInt ( stepString , 10 , 64 )
if err != nil {
2022-11-21 15:07:28 +03:00
return "" , 0 , herrors . E ( op , fmt . Errorf ( "not a valid input for steps/version: %w" , err ) )
2018-06-24 16:40:48 +03:00
}
return migrationName , step , nil
}