cli: migrate delete command, update migration state on server after squash

It contains 2 flags

all   -  To delete all migrations locally and on database
version - To delete a single migration locally and on database

Usage :
`hasura migrate delete --all`
`hasura migrate delete --version <version_number>`

Additional fix :
The `migrate squash`  will deletes the migration history on server after squashing if user opts to delete the migrations.

closes https://github.com/hasura/graphql-engine-mono/issues/292
closes https://github.com/hasura/graphql-engine/issues/5373
closes https://github.com/hasura/graphql-engine/issues/6434

Co-authored-by: Aravind K P <8335904+scriptonist@users.noreply.github.com>
GitOrigin-RevId: fa7ceae7a1970d6724fb601a147900e880ad2e6f
This commit is contained in:
Kali Vara Purushotham Santhati 2021-05-24 08:03:45 +05:30 committed by hasura-bot
parent feb08d8998
commit 5e92ce028e
9 changed files with 414 additions and 14 deletions

View File

@ -40,7 +40,7 @@
- console: allow editing sources configuration
- console: show db version and source details in manage db page
- console: add one-to-one relationships support
- cli: add `-o`/`--output` flag for `metadata` `apply` & `export` subcommands
- cli: add `-o`/`--output` flag for `hasura metadata` `apply` & `export` subcommands
```
# export metadata and write to stdout
$ hasura metadata export -o json
@ -50,6 +50,7 @@ $ hasura metadata export -o json
- cli: fix bug caused by usage of space character in database name (#6852)
- cli: fix issues with generated filepaths in windows (#6813)
- cli: add warning for incompatible pro plugin version
- cli: add new sub command `delete` to `hasura migrate`
## v2.0.0-alpha.10

View File

@ -46,7 +46,7 @@ var _ = Describe("actions_codegen", func() {
Args: []string{"actions", "codegen"},
WorkingDirectory: dirName,
})
Eventually(session, 60*40).Should(Exit(0))
Eventually(session, 60*60).Should(Exit(0))
Eventually(session.Wait().Err.Contents()).Should(ContainSubstring("Codegen files generated at codegen"))
})
})

View File

@ -65,6 +65,7 @@ func NewMigrateCmd(ec *cli.ExecutionContext) *cobra.Command {
newMigrateStatusCmd(ec),
newMigrateCreateCmd(ec),
newMigrateSquashCmd(ec),
newMigrateDeleteCmd(ec),
)
return migrateCmd

View File

@ -0,0 +1,152 @@
package commands
import (
"fmt"
"path/filepath"
"strconv"
"github.com/hasura/graphql-engine/cli"
"github.com/hasura/graphql-engine/cli/migrate"
mig "github.com/hasura/graphql-engine/cli/migrate/cmd"
"github.com/hasura/graphql-engine/cli/util"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
func newMigrateDeleteCmd(ec *cli.ExecutionContext) *cobra.Command {
opts := &MigrateDeleteOptions{
EC: ec,
}
migrateDeleteCmd := &cobra.Command{
Use: "delete",
Short: "(PREVIEW) clear migrations from local project and server",
Example: `
# Usage to delete a version:
hasura migrate delete --version <version_delete> --database-name default
# Usage to delete all versions
hasura migrate delete --all`,
SilenceUsage: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
ec.Logger.Warn("[PREVIEW] this command is in preview. usage may change in future\n")
if err := validateConfigV3Flags(cmd, ec); err != nil {
return err
}
if !cmd.Flags().Changed("all") && !cmd.Flags().Changed("version") {
return fmt.Errorf("at least one flag [--all , --version] should be set")
}
if cmd.Flags().Changed("all") && cmd.Flags().Changed("version") {
return fmt.Errorf("only one of [--all , --version] should be set")
}
if cmd.Flags().Changed("all") && !opts.force {
confirmation, err := util.GetYesNoPrompt("clear all migrations of database and it's history on the server?")
if err != nil {
return fmt.Errorf("error getting user input: %w", err)
}
if confirmation == "n" {
return nil
}
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
opts.Source = ec.Source
if ec.Config.Version >= cli.V3 {
var err error
opts.EC.Spin("Removing migrations")
err = opts.Run()
opts.EC.Spinner.Stop()
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
return err
}
opts.EC.Spin("Removing migrations")
err := opts.Run()
opts.EC.Spinner.Stop()
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
return nil
},
}
f := migrateDeleteCmd.Flags()
f.Uint64Var(&opts.version, "version", 0, "deletes the specified version in migrations")
f.BoolVar(&opts.all, "all", false, "clears all migrations for selected database")
f.BoolVar(&opts.force, "force", false, "when set executes operation without any confirmation")
return migrateDeleteCmd
}
type MigrateDeleteOptions struct {
EC *cli.ExecutionContext
version uint64
all bool
force bool
Source cli.Source
}
func (o *MigrateDeleteOptions) Run() error {
o.EC.Spin("Deleting migration...")
defer o.EC.Spinner.Stop()
migrateDrv, err := migrate.NewMigrate(o.EC, true, o.Source.Name, o.Source.Kind)
if err != nil {
return fmt.Errorf("error in creation of new migrate instance %w", err)
}
status, err := migrateDrv.GetStatus()
if err != nil {
return fmt.Errorf("error while retrieving migration status %w", err)
}
if !o.all {
if _, ok := status.Migrations[o.version]; !ok {
return fmt.Errorf("version %v not found", o.version)
}
err := DeleteVersions(o.EC, []uint64{o.version}, o.Source)
if err != nil {
o.EC.Logger.Warn(errors.Wrap(err, "error in deletion of migration in source"))
}
versions := []uint64{o.version}
err = migrateDrv.RemoveVersions(versions)
} else if o.all {
var sourceVersions, serverVersions []uint64
for k, v := range status.Migrations {
if v.IsApplied {
serverVersions = append(serverVersions, k)
}
if v.IsPresent {
sourceVersions = append(sourceVersions, k)
}
}
// delete version history on server
err = migrateDrv.RemoveVersions(serverVersions)
if err != nil {
return fmt.Errorf("error removing migration from server: %w", err)
}
// delete migrations history in project
err = DeleteVersions(o.EC, sourceVersions, o.Source)
if err != nil {
return fmt.Errorf("error removing migration from project: %w", err)
}
}
o.EC.Logger.Infof("Deleted migrations")
return nil
}
func DeleteVersions(ec *cli.ExecutionContext, versions []uint64, source cli.Source) error {
for _, v := range versions {
delOptions := mig.CreateOptions{
Version: strconv.FormatUint(v, 10),
Directory: filepath.Join(ec.MigrationDir, source.Name),
}
err := delOptions.Delete()
if err != nil {
return fmt.Errorf("unable to delete migrations from project for: %v : %w", v, err)
}
}
return nil
}

View File

@ -0,0 +1,231 @@
package commands
import (
"fmt"
"os/exec"
"path/filepath"
"strings"
"github.com/hasura/graphql-engine/cli/internal/testutil"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gbytes"
. "github.com/onsi/gomega/gexec"
)
var _ = Describe("migrate_delete", func() {
var session *Session
var teardown func()
var hgeEndpoint string
BeforeEach(func() {
hgeEndPort, teardownHGE := testutil.StartHasura(GinkgoT(), testutil.HasuraVersion)
hgeEndpoint = fmt.Sprintf("http://0.0.0.0:%s", hgeEndPort)
teardown = func() {
session.Kill()
teardownHGE()
}
})
AfterEach(func() {
teardown()
})
Context("migrate delete --all", func() {
It("should delete the migrations on server and on source ", func() {
projectDirectory := testutil.RandDirName()
testutil.RunCommandAndSucceed(testutil.CmdOpts{
Args: []string{"init", projectDirectory},
})
editEndpointInConfig(filepath.Join(projectDirectory, defaultConfigFilename), hgeEndpoint)
session = testutil.RunCommandAndSucceed(testutil.CmdOpts{
Args: []string{"migrate", "create", "schema_creation", "--up-sql", "create schema \"testing\";", "--down-sql", "drop schema \"testing\" cascade;", "--database-name", "default"},
WorkingDirectory: projectDirectory,
})
str := string(session.Err.Contents())
i := strings.Index(str, "\"version\"")
version := str[i+10 : i+23]
session = testutil.Hasura(testutil.CmdOpts{
Args: []string{"migrate", "apply", "--database-name", "default"},
WorkingDirectory: projectDirectory,
})
wantKeywordList := []string{
".*Applying migrations...*.",
".*migrations*.",
".*applied*.",
}
for _, keyword := range wantKeywordList {
Eventually(session.Err, 60*40).Should(Say(keyword))
}
session = testutil.Hasura(testutil.CmdOpts{
Args: []string{"migrate", "delete", "--all"},
WorkingDirectory: projectDirectory,
})
Eventually(session.Err, 60*40).Should(Say("--database-name flag is required"))
args := strings.Join([]string{"yes", "|", testutil.CLIBinaryPath, "migrate", "delete", "--all", "--database-name", "default"}, " ")
cmd := exec.Command("bash", "-c", args)
cmd.Dir = projectDirectory
session, err := Start(
cmd,
NewPrefixedWriter(testutil.DebugOutPrefix, GinkgoWriter),
NewPrefixedWriter(testutil.DebugErrPrefix, GinkgoWriter),
)
Expect(err).To(BeNil())
Eventually(session.Err, 60*40).Should(Say("Deleted migrations"))
session = testutil.Hasura(testutil.CmdOpts{
Args: []string{"migrate", "status", "--database-name", "default"},
WorkingDirectory: projectDirectory,
})
Eventually(session.Err, 60*40).ShouldNot(Say(version))
Eventually(session, 60*50).Should(Exit(0))
})
})
Context("migrate delete --version <version>", func() {
It("should delete the migrations on server and on source ", func() {
dirName := testutil.RandDirName()
testutil.RunCommandAndSucceed(testutil.CmdOpts{
Args: []string{"init", dirName},
})
editEndpointInConfig(filepath.Join(dirName, defaultConfigFilename), hgeEndpoint)
session = testutil.RunCommandAndSucceed(testutil.CmdOpts{
Args: []string{"migrate", "create", "schema_creation", "--up-sql", "create schema \"testing\";", "--down-sql", "drop schema \"testing\" cascade;", "--database-name", "default"},
WorkingDirectory: dirName,
})
str := string(session.Err.Contents())
i := strings.Index(str, "\"version\"")
version := str[i+10 : i+23]
session = testutil.Hasura(testutil.CmdOpts{
Args: []string{"migrate", "apply", "--database-name", "default"},
WorkingDirectory: dirName,
})
wantKeywordList := []string{
".*Applying migrations...*.",
".*migrations*.",
".*applied*.",
}
for _, keyword := range wantKeywordList {
Eventually(session.Err, 60*40).Should(Say(keyword))
}
session = testutil.Hasura(testutil.CmdOpts{
Args: []string{"migrate", "delete", "--version", version, "--database-name", "default"},
WorkingDirectory: dirName,
})
Eventually(session.Err, 60*40).Should(Say("Deleted migrations"))
session = testutil.Hasura(testutil.CmdOpts{
Args: []string{"migrate", "status", "--database-name", "default"},
WorkingDirectory: dirName,
})
Eventually(session.Err, 60*40).ShouldNot(Say(version))
Eventually(session, 60*50).Should(Exit(0))
})
})
Context("migrate delete --version <version> (config v2)", func() {
It("should delete the migrations on server and on source ", func() {
projectDirectory := testutil.RandDirName()
testutil.RunCommandAndSucceed(testutil.CmdOpts{
Args: []string{"init", projectDirectory, "--version", "2"},
})
editEndpointInConfig(filepath.Join(projectDirectory, defaultConfigFilename), hgeEndpoint)
session = testutil.RunCommandAndSucceed(testutil.CmdOpts{
Args: []string{"migrate", "create", "schema_creation", "--up-sql", "create schema \"testing\";", "--down-sql", "drop schema \"testing\" cascade;"},
WorkingDirectory: projectDirectory,
})
str := string(session.Err.Contents())
i := strings.Index(str, "\"version\"")
version := str[i+10 : i+23]
session = testutil.Hasura(testutil.CmdOpts{
Args: []string{"migrate", "apply"},
WorkingDirectory: projectDirectory,
})
wantKeywordList := []string{
".*Applying migrations...*.",
".*migrations*.",
".*applied*.",
}
for _, keyword := range wantKeywordList {
Eventually(session.Err, 60*40).Should(Say(keyword))
}
session = testutil.Hasura(testutil.CmdOpts{
Args: []string{"migrate", "delete", "--version", version},
WorkingDirectory: projectDirectory,
})
Eventually(session.Err, 60*40).Should(Say("Deleted migrations"))
session = testutil.Hasura(testutil.CmdOpts{
Args: []string{"migrate", "status"},
WorkingDirectory: projectDirectory,
})
Eventually(session.Err, 60*40).ShouldNot(Say(version))
Eventually(session, 60*50).Should(Exit(0))
})
})
Context("migrate delete --all (config v2)", func() {
It("should delete the migrations on server and on source ", func() {
projectDirectory := testutil.RandDirName()
testutil.RunCommandAndSucceed(testutil.CmdOpts{
Args: []string{"init", projectDirectory, "--version", "2"},
})
editEndpointInConfig(filepath.Join(projectDirectory, defaultConfigFilename), hgeEndpoint)
session = testutil.RunCommandAndSucceed(testutil.CmdOpts{
Args: []string{"migrate", "create", "schema_creation", "--up-sql", "create schema \"testing\";", "--down-sql", "drop schema \"testing\" cascade;"},
WorkingDirectory: projectDirectory,
})
str := string(session.Err.Contents())
i := strings.Index(str, "\"version\"")
version := str[i+10 : i+23]
session = testutil.Hasura(testutil.CmdOpts{
Args: []string{"migrate", "apply"},
WorkingDirectory: projectDirectory,
})
wantKeywordList := []string{
".*Applying migrations...*.",
".*migrations*.",
".*applied*.",
}
for _, keyword := range wantKeywordList {
Eventually(session.Err, 60*40).Should(Say(keyword))
}
args := []string{"migrate", "delete", "--all", "--force"}
cmd := exec.Command(testutil.CLIBinaryPath, args...)
cmd.Dir = projectDirectory
session, err := Start(
cmd,
NewPrefixedWriter(testutil.DebugOutPrefix, GinkgoWriter),
NewPrefixedWriter(testutil.DebugErrPrefix, GinkgoWriter),
)
Expect(err).To(BeNil())
Eventually(session.Err, 60*40).Should(Say("Deleted migrations"))
session = testutil.Hasura(testutil.CmdOpts{
Args: []string{"migrate", "status"},
WorkingDirectory: projectDirectory,
})
Eventually(session.Err, 60*40).ShouldNot(Say(version))
Eventually(session, 60*50).Should(Exit(0))
})
})
})

View File

@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"path/filepath"
"strconv"
"strings"
"text/tabwriter"
@ -98,15 +97,23 @@ func (o *migrateSquashOptions) run() error {
}
}
for _, v := range versions {
delOptions := mig.CreateOptions{
Version: strconv.FormatInt(v, 10),
Directory: filepath.Join(o.EC.MigrationDir, o.Source.Name),
}
err = delOptions.Delete()
if err != nil {
return errors.Wrap(err, "unable to delete source file")
var uversions []uint64
for _, version := range versions {
if version < 0 {
return fmt.Errorf("operation failed foound version value should >= 0, which is not expected")
}
uversions = append(uversions, uint64(version))
}
// If the first argument is true then it deletes all the migration versions
err = DeleteVersions(o.EC, uversions, o.Source)
if err != nil {
return err
}
err = migrateDrv.RemoveVersions(uversions)
if err != nil {
return err
}
return nil
}

View File

@ -62,6 +62,16 @@ var _ = Describe("migrate_squash", func() {
Eventually(session.Err, 60*40).Should(Say(keyword))
}
Eventually(session, 60*40).Should(Exit(0))
// verify files were deleted
v := strings.Split(matches[0], ":")[1]
Expect(filepath.Join(dirName, "migrations", "default", v)).ShouldNot(BeADirectory())
// verify squashed migrations are deleted in statestore
session = testutil.Hasura(testutil.CmdOpts{
Args: []string{"migrate", "status", "--database-name", "default"},
WorkingDirectory: dirName,
})
Eventually(session, 60).Should(Exit(0))
Eventually(session.Out.Contents()).ShouldNot(ContainSubstring(v))
})
})
})

View File

@ -402,7 +402,6 @@ func (h *HasuraDB) Drop() error {
func (h *HasuraDB) sendSchemaDumpQuery(m interface{}) (resp *http.Response, body []byte, err error) {
request := h.config.Req.Clone()
request = request.Post(h.config.pgDumpURL.String()).Send(m)
for headerName, headerValue := range h.config.Headers {

View File

@ -11,11 +11,10 @@ import (
"runtime"
"strings"
"github.com/spf13/afero"
"github.com/hasura/graphql-engine/cli/migrate/source"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/afero"
)
type File struct {