From 2def1585ccefa491c10b099fa7f85bbb6bd7539f Mon Sep 17 00:00:00 2001 From: Jacek Wysocki Date: Wed, 9 Feb 2022 12:09:01 +0100 Subject: [PATCH] feat: added migrator for 0.8.8 (#950) * feat: added migrator for 0.8.8 * fix: fixed imports in process pkg * fix: fixed imports in process pkg * feat: ui in migrate command --- cmd/kubectl-testkube/commands/migrate.go | 49 +++++++++ cmd/kubectl-testkube/commands/root.go | 1 + internal/migrations/init.go | 9 ++ internal/migrations/version_0.8.8.go | 33 ++++++ pkg/migrator/migrator.go | 81 +++++++++++++++ pkg/migrator/migrator_test.go | 122 +++++++++++++++++++++++ pkg/process/exec.go | 18 ++++ pkg/version/version.go | 9 ++ 8 files changed, 322 insertions(+) create mode 100644 cmd/kubectl-testkube/commands/migrate.go create mode 100644 internal/migrations/init.go create mode 100644 internal/migrations/version_0.8.8.go create mode 100644 pkg/migrator/migrator.go create mode 100644 pkg/migrator/migrator_test.go diff --git a/cmd/kubectl-testkube/commands/migrate.go b/cmd/kubectl-testkube/commands/migrate.go new file mode 100644 index 00000000000..9ac0f02e196 --- /dev/null +++ b/cmd/kubectl-testkube/commands/migrate.go @@ -0,0 +1,49 @@ +package commands + +import ( + "fmt" + + "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" + "github.com/kubeshop/testkube/internal/migrations" + "github.com/kubeshop/testkube/pkg/ui" + "github.com/spf13/cobra" +) + +func NewMigrateCmd() *cobra.Command { + var namespace string + cmd := &cobra.Command{ + Use: "migrate", + Short: "migrate command", + Long: `migrate command manages migrations`, + Run: func(cmd *cobra.Command, args []string) { + ui.Logo() + + client, _ := common.GetClient(cmd) + info, err := client.GetServerInfo() + ui.ExitOnError("getting server info", err) + + if info.Version == "" { + ui.Failf("Can't detect cluster version") + } + + migrator := migrations.Migrator + ui.Info("Available migrations for", info.Version) + migrations := migrator.GetValidMigrations(info.Version) + if len(migrations) == 0 { + ui.Warn("No migrations available for", info.Version) + } + + for _, migration := range migrations { + fmt.Printf("- %+v - %s\n", migration.Version(), migration.Info()) + } + + err = migrator.Run(info.Version) + ui.ExitOnError("running migrations", err) + ui.Success("All migrations executed successfully") + }, + } + + cmd.Flags().StringVar(&namespace, "namespace", "testkube", "testkube namespace") + + return cmd +} diff --git a/cmd/kubectl-testkube/commands/root.go b/cmd/kubectl-testkube/commands/root.go index 889abc53c7a..89e6cded35f 100644 --- a/cmd/kubectl-testkube/commands/root.go +++ b/cmd/kubectl-testkube/commands/root.go @@ -29,6 +29,7 @@ func init() { RootCmd.AddCommand(NewExecutorsCmd()) RootCmd.AddCommand(NewArtifactsCmd()) RootCmd.AddCommand(NewTestsCmd()) + RootCmd.AddCommand(NewMigrateCmd()) } var RootCmd = &cobra.Command{ diff --git a/internal/migrations/init.go b/internal/migrations/init.go new file mode 100644 index 00000000000..35c2b9a2244 --- /dev/null +++ b/internal/migrations/init.go @@ -0,0 +1,9 @@ +package migrations + +import "github.com/kubeshop/testkube/pkg/migrator" + +var Migrator migrator.Migrator + +func init() { + Migrator = *migrator.NewMigrator() +} diff --git a/internal/migrations/version_0.8.8.go b/internal/migrations/version_0.8.8.go new file mode 100644 index 00000000000..7d78e0b3eb6 --- /dev/null +++ b/internal/migrations/version_0.8.8.go @@ -0,0 +1,33 @@ +package migrations + +// add migration to global migrator +func init() { + Migrator.Add(NewVersion_0_8_8()) +} + +func NewVersion_0_8_8() *Version_0_8_8 { + return &Version_0_8_8{} +} + +type Version_0_8_8 struct { +} + +func (m *Version_0_8_8) Version() string { + return "0.8.8" +} +func (m *Version_0_8_8) Migrate() error { + commands := []string{ + `kubectl annotate --overwrite crds executors.executor.testkube.io meta.helm.sh/release-name="testkube" meta.helm.sh/release-namespace="testkube"`, + `kubectl annotate --overwrite crds tests.tests.testkube.io meta.helm.sh/release-name="testkube" meta.helm.sh/release-namespace="testkube"`, + `kubectl annotate --overwrite crds scripts.tests.testkube.io meta.helm.sh/release-name="testkube" meta.helm.sh/release-namespace="testkube"`, + `kubectl label --overwrite crds executors.executor.testkube.io app.kubernetes.io/managed-by=Helm`, + `kubectl label --overwrite crds tests.tests.testkube.io app.kubernetes.io/managed-by=Helm`, + `kubectl label --overwrite crds scripts.tests.testkube.io app.kubernetes.io/managed-by=Helm`, + } + + _, err := Migrator.ExecuteCommands(commands) + return err +} +func (m *Version_0_8_8) Info() string { + return "Adding labels and annotations to Testkube CRDs" +} diff --git a/pkg/migrator/migrator.go b/pkg/migrator/migrator.go new file mode 100644 index 00000000000..76a94bd9949 --- /dev/null +++ b/pkg/migrator/migrator.go @@ -0,0 +1,81 @@ +package migrator + +import ( + "fmt" + "strings" + + "github.com/kubeshop/testkube/pkg/log" + "github.com/kubeshop/testkube/pkg/process" + "github.com/kubeshop/testkube/pkg/version" + "go.uber.org/zap" +) + +type Migration interface { + Migrate() error + Version() string + Info() string +} + +func NewMigrator() *Migrator { + return &Migrator{ + Log: log.DefaultLogger, + } +} + +type Migrator struct { + Migrations []Migration + Log *zap.SugaredLogger +} + +func (m *Migrator) Add(migration Migration) { + m.Migrations = append(m.Migrations, migration) +} + +func (m *Migrator) GetValidMigrations(currentVersion string) (migrations []Migration) { + for _, migration := range m.Migrations { + if ok, err := m.IsValid(migration.Version(), currentVersion); ok && err == nil { + migrations = append(migrations, migration) + } + } + + return +} + +func (m *Migrator) Run(currentVersion string) error { + for _, migration := range m.GetValidMigrations(currentVersion) { + err := migration.Migrate() + if err != nil { + return err + } + } + + return nil +} + +// IsValid checks if versions constraints are met, assuming that currentVersion +// is just updated version and it should be taken for migration +func (m Migrator) IsValid(migrationVersion, currentVersion string) (bool, error) { + + // clean possible v prefixes + migrationVersion = strings.TrimPrefix(migrationVersion, "v") + currentVersion = strings.TrimPrefix(currentVersion, "v") + + if migrationVersion == "" || currentVersion == "" { + return false, fmt.Errorf("empty version migration:'%s', current:'%s'", migrationVersion, currentVersion) + } + + return version.Lte(currentVersion, migrationVersion) +} + +func (m Migrator) ExecuteCommands(commands []string) (outputs []string, err error) { + for _, command := range commands { + out, err := process.ExecuteString(command) + if err != nil { + return outputs, err + } + + outputs = append(outputs, string(out)) + } + + return outputs, nil +} diff --git a/pkg/migrator/migrator_test.go b/pkg/migrator/migrator_test.go new file mode 100644 index 00000000000..4cd56790613 --- /dev/null +++ b/pkg/migrator/migrator_test.go @@ -0,0 +1,122 @@ +package migrator + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ErrMigrationFailed = fmt.Errorf("migration failed") + +func TestMigrator(t *testing.T) { + + t.Run("migrate versions one after another", func(t *testing.T) { + // given + migrator := NewMigrator() + migrator.Add(&Migr1{}) + migrator.Add(&Migr2{}) + migrator.Add(&Migr3{}) + + // when + migrator.Run("0.0.2") + + // then + assert.Equal(t, migrator.Migrations[0].(*Migr1).Run, false) + assert.Equal(t, migrator.Migrations[1].(*Migr2).Run, true) + assert.Equal(t, migrator.Migrations[2].(*Migr3).Run, true) + }) + + t.Run("migrate mixed versions", func(t *testing.T) { + // given + migrator := NewMigrator() + migrator.Add(&Migr3{}) + migrator.Add(&Migr1{}) + migrator.Add(&Migr2{}) + migrator.Add(&Migr1{}) + + // when + migrator.Run("0.0.2") + + // then + assert.Equal(t, migrator.Migrations[0].(*Migr3).Run, true) + assert.Equal(t, migrator.Migrations[1].(*Migr1).Run, false) + assert.Equal(t, migrator.Migrations[2].(*Migr2).Run, true) + assert.Equal(t, migrator.Migrations[3].(*Migr1).Run, false) + }) + + t.Run("failed migration returns error", func(t *testing.T) { + // given + migrator := NewMigrator() + migrator.Add(&Migr1{}) + migrator.Add(&MigrFailed{}) + migrator.Add(&Migr1{}) + + // when + err := migrator.Run("0.0.1") + + // then + assert.Error(t, err, ErrMigrationFailed) + }) + +} + +type Migr1 struct { + Run bool +} + +func (m *Migr1) Version() string { + return "0.0.1" +} +func (m *Migr1) Migrate() error { + m.Run = true + return nil +} +func (m *Migr1) Info() string { + return "some migration description 1" +} + +type Migr2 struct { + Run bool +} + +func (m *Migr2) Version() string { + return "0.0.2" +} +func (m *Migr2) Migrate() error { + m.Run = true + return nil +} +func (m *Migr2) Info() string { + return "some migration description 2" +} + +type Migr3 struct { + Run bool +} + +func (m *Migr3) Version() string { + return "0.0.3" +} +func (m *Migr3) Migrate() error { + m.Run = true + return nil +} +func (m *Migr3) Info() string { + return "some migration description 3" +} + +type MigrFailed struct { + Run bool +} + +func (m *MigrFailed) Version() string { + return "0.0.1" +} +func (m *MigrFailed) Migrate() error { + m.Run = true + return ErrMigrationFailed +} +func (m *MigrFailed) Info() string { + return "some failed migration" +} diff --git a/pkg/process/exec.go b/pkg/process/exec.go index 425ff46a13d..62913a990b1 100644 --- a/pkg/process/exec.go +++ b/pkg/process/exec.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os/exec" + "strings" ) // Execute runs system command and returns whole output also in case of error @@ -72,3 +73,20 @@ func ExecuteAsyncInDir(dir string, command string, arguments ...string) (cmd *ex return cmd, nil } + +func ExecuteString(command string) (out []byte, err error) { + parts := strings.Split(command, " ") + if len(parts) == 1 { + out, err = Execute(parts[0]) + } else if len(parts) > 1 { + out, err = Execute(parts[0], parts[1:]...) + } else { + return out, fmt.Errorf("invalid command to run '%s'", command) + } + + if err != nil { + return out, fmt.Errorf("error: %w, output: %s", err, out) + } + + return out, nil +} diff --git a/pkg/version/version.go b/pkg/version/version.go index bee17311ec1..ea405a386b9 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -105,6 +105,15 @@ func Lt(version1, version2 string) (bool, error) { return v1.LessThan(v2), nil } +func Lte(version1, version2 string) (bool, error) { + ok, err := Lt(version1, version2) + if err != nil { + return false, err + } + + return ok || version1 == version2, nil +} + func validateVersionPostion(kind string) error { if kind == Major || kind == Minor || kind == Patch { return nil