This commit is contained in:
Adrian Stobbe 2023-07-15 07:29:32 +02:00
parent 7fcf7e15c6
commit ad56360d5f
10 changed files with 272 additions and 3 deletions

View File

@ -111,6 +111,12 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, fileHandler file.Hand
return fmt.Errorf("upgrading measurements: %w", err) return fmt.Errorf("upgrading measurements: %w", err)
} }
migrateIAM := getIAMMigrateCmd(cmd, u.upgrader.GetTerraformUpgrader(), conf, flags, u.upgrader.GetUpgradeID())
if err := u.executeMigration(cmd, fileHandler, migrateIAM, flags); err != nil {
return fmt.Errorf("executing IAM migration: %w", err)
}
//
if err := u.migrateTerraform(cmd, fileHandler, u.imageFetcher, conf, flags); err != nil { if err := u.migrateTerraform(cmd, fileHandler, u.imageFetcher, conf, flags); err != nil {
return fmt.Errorf("performing Terraform migrations: %w", err) return fmt.Errorf("performing Terraform migrations: %w", err)
} }
@ -211,6 +217,60 @@ func (u *upgradeApplyCmd) migrateTerraform(cmd *cobra.Command, file file.Handler
return nil return nil
} }
func getIAMMigrateCmd(cmd *cobra.Command, tfClient *terraform.Client, conf *config.Config, flags upgradeApplyFlags, upgradeID string) *terraform.IAMMigrateCmd {
// Check if there are any Terraform migrations to apply
outWriter := cmd.OutOrStdout()
migrateCmd := terraform.NewIAMMigrateCmd(
tfClient,
upgradeID,
conf.GetProvider(),
flags.terraformLogLevel,
outWriter,
)
return migrateCmd
}
func (u *upgradeApplyCmd) executeMigration(cmd *cobra.Command, file file.Handler, migrateCmd terraform.MigrationCmd, flags upgradeApplyFlags) error {
hasDiff, err := migrateCmd.Plan(cmd.Context()) // u.upgrader.PlanTerraformMigrations(cmd.Context(), opts)
if err != nil {
return fmt.Errorf("planning terraform migrations: %w", err)
}
if hasDiff {
// If there are any Terraform migrations to apply, ask for confirmation
fmt.Fprintf(cmd.OutOrStdout(), "The %s upgrade requires a migration of Constellation cloud resources by applying an updated Terraform template. Please manually review the suggested changes below.\n", migrateCmd.String())
if !flags.yes {
ok, err := askToConfirm(cmd, fmt.Sprintf("Do you want to apply the %s?", migrateCmd.String()))
if err != nil {
return fmt.Errorf("asking for confirmation: %w", err)
}
if !ok {
cmd.Println("Aborting upgrade.")
if err := u.upgrader.CleanUpTerraformMigrations(file); err != nil {
return fmt.Errorf("cleaning up workspace: %w", err)
}
return fmt.Errorf("aborted by user")
}
}
u.log.Debugf("Applying Terraform %s migrations", migrateCmd.String())
// .ApplyMigration()
err := migrateCmd.Apply(cmd.Context(), file) // u.upgrader.ApplyTerraformMigrations(cmd.Context(), file, opts)
if err != nil {
return fmt.Errorf("applying terraform migrations: %w", err)
}
// TODO write this outside of apply migration
upgradeOutputFile := constants.TerraformMigrationOutputFile
cmd.Printf("Terraform migrations applied successfully and output written to: %s\n"+
"A backup of the pre-upgrade Terraform state has been written to: %s\n",
upgradeOutputFile, filepath.Join(constants.UpgradeDir, constants.TerraformUpgradeBackupDir)) // TODO include log of file write where the action happens?
} else {
u.log.Debugf("No Terraform diff detected")
}
return nil
}
// parseTerraformUpgradeVars parses the variables required to execute the Terraform script with. // parseTerraformUpgradeVars parses the variables required to execute the Terraform script with.
func parseTerraformUpgradeVars(cmd *cobra.Command, conf *config.Config, fetcher imageFetcher) (terraform.Variables, error) { func parseTerraformUpgradeVars(cmd *cobra.Command, conf *config.Config, fetcher imageFetcher) (terraform.Variables, error) {
// Fetch variables to execute Terraform script with // Fetch variables to execute Terraform script with
@ -446,6 +506,8 @@ type cloudUpgrader interface {
CheckTerraformMigrations(fileHandler file.Handler) error CheckTerraformMigrations(fileHandler file.Handler) error
CleanUpTerraformMigrations(fileHandler file.Handler) error CleanUpTerraformMigrations(fileHandler file.Handler) error
AddManualStateMigration(migration terraform.StateMigration) AddManualStateMigration(migration terraform.StateMigration)
GetTerraformUpgrader() *terraform.Client
GetUpgradeID() string
} }
func toPtr[T any](v T) *T { func toPtr[T any](v T) *T {

View File

@ -153,6 +153,14 @@ func NewUpgrader(ctx context.Context, outWriter io.Writer, log debugLog, upgrade
return u, nil return u, nil
} }
func (u *Upgrader) GetTerraformUpgrader() *terraform.Client {
return u.tfClient
}
func (u *Upgrader) GetUpgradeID() string {
return u.upgradeID
}
// AddManualStateMigration adds a manual state migration to the Terraform client. // AddManualStateMigration adds a manual state migration to the Terraform client.
// TODO(AB#3248): Remove this method after we can assume that all existing clusters have been migrated. // TODO(AB#3248): Remove this method after we can assume that all existing clusters have been migrated.
func (u *Upgrader) AddManualStateMigration(migration terraform.StateMigration) { func (u *Upgrader) AddManualStateMigration(migration terraform.StateMigration) {
@ -175,10 +183,15 @@ func (u *Upgrader) CleanUpTerraformMigrations(fileHandler file.Handler) error {
// If a diff exists, it's being written to the upgrader's output writer. It also returns // If a diff exists, it's being written to the upgrader's output writer. It also returns
// a bool indicating whether a diff exists. // a bool indicating whether a diff exists.
func (u *Upgrader) PlanTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (bool, error) { func (u *Upgrader) PlanTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (bool, error) {
return u.tfUpgrader.PlanTerraformMigrations(ctx, opts, u.upgradeID) hasDiff, err := u.tfUpgrader.PlanIAMMigration(ctx, opts.CSP, opts.LogLevel, u.upgradeID)
if err != nil {
return false, fmt.Errorf("planning terraform migrations: %w", err)
}
return hasDiff, nil
// return u.tfUpgrader.PlanTerraformMigrations(ctx, opts, u.upgradeID)
} }
// ApplyTerraformMigrations applies the migerations planned by PlanTerraformMigrations. // ApplyTerraformMigrations applies the migrations planned by PlanTerraformMigrations.
// If PlanTerraformMigrations has not been executed before, it will return an error. // If PlanTerraformMigrations has not been executed before, it will return an error.
// In case of a successful upgrade, the output will be written to the specified file and the old Terraform directory is replaced // In case of a successful upgrade, the output will be written to the specified file and the old Terraform directory is replaced
// By the new one. // By the new one.

View File

@ -4,6 +4,7 @@ load("//bazel/go:go_test.bzl", "go_test")
go_library( go_library(
name = "terraform", name = "terraform",
srcs = [ srcs = [
"iammigrate.go",
"loader.go", "loader.go",
"logging.go", "logging.go",
"terraform.go", "terraform.go",

View File

@ -0,0 +1,98 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package terraform
import (
"context"
"fmt"
"io"
"path/filepath"
"strings"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
)
type tfClient interface {
PrepareIAMUpgradeWorkspace(rootDir, workingDir, newWorkingDir, backupDir string) error
Plan(ctx context.Context, logLevel LogLevel, planFile string) (bool, error)
ShowPlan(ctx context.Context, logLevel LogLevel, planFile string, outWriter io.Writer) error
CreateIAMConfig(ctx context.Context, csp cloudprovider.Provider, logLevel LogLevel) (IAMOutput, error)
}
type MigrationCmd interface {
Plan(ctx context.Context) (bool, error)
Apply(ctx context.Context, fileHandler file.Handler) error
String() string
}
type IAMMigrateCmd struct {
tf tfClient
upgradeID string
csp cloudprovider.Provider
logLevel LogLevel
outWriter io.Writer
}
func NewIAMMigrateCmd(tf tfClient, upgradeID string, csp cloudprovider.Provider, logLevel LogLevel, outWriter io.Writer) *IAMMigrateCmd {
return &IAMMigrateCmd{
tf: tf,
upgradeID: upgradeID,
csp: csp,
logLevel: logLevel,
outWriter: outWriter,
}
}
func (c *IAMMigrateCmd) String() string {
return "iam migration"
}
func (c *IAMMigrateCmd) Plan(ctx context.Context) (bool, error) {
err := c.tf.PrepareIAMUpgradeWorkspace(
filepath.Join("terraform", "iam", strings.ToLower(c.csp.String())),
constants.TerraformIAMWorkingDir,
filepath.Join(constants.UpgradeDir, c.upgradeID, constants.TerraformIAMUpgradeWorkingDir),
filepath.Join(constants.UpgradeDir, c.upgradeID, constants.TerraformUpgradeBackupDir), // TODO: use IAM backup dir
)
if err != nil {
return false, fmt.Errorf("preparing terraform workspace: %w", err)
}
hasDiff, err := c.tf.Plan(ctx, c.logLevel, constants.TerraformUpgradePlanFile)
if err != nil {
return false, fmt.Errorf("terraform plan: %w", err)
}
if hasDiff {
if err := c.tf.ShowPlan(ctx, c.logLevel, constants.TerraformUpgradePlanFile, c.outWriter); err != nil {
return false, fmt.Errorf("terraform show plan: %w", err)
}
}
return hasDiff, nil
}
func (c *IAMMigrateCmd) Apply(ctx context.Context, fileHandler file.Handler) error {
_, err := c.tf.CreateIAMConfig(ctx, c.csp, c.logLevel)
// TODO: put in template?
if err := fileHandler.RemoveAll(constants.TerraformIAMWorkingDir); err != nil {
return fmt.Errorf("removing old terraform directory: %w", err)
}
if err := fileHandler.CopyDir(filepath.Join(constants.UpgradeDir, c.upgradeID, constants.TerraformIAMUpgradeWorkingDir), constants.TerraformIAMWorkingDir); err != nil {
return fmt.Errorf("replacing old terraform directory with new one: %w", err)
}
if err := fileHandler.RemoveAll(filepath.Join(constants.UpgradeDir, c.upgradeID, constants.TerraformIAMUpgradeWorkingDir)); err != nil {
return fmt.Errorf("removing terraform upgrade directory: %w", err)
}
return err
}

View File

@ -102,6 +102,18 @@ func (c *Client) PrepareUpgradeWorkspace(path, oldWorkingDir, newWorkingDir, bac
return c.writeVars(vars) return c.writeVars(vars)
} }
// PrepareIAMUpgradeWorkspace prepares a Terraform workspace for a Constellation IAM upgrade.
func (c *Client) PrepareIAMUpgradeWorkspace(path, oldWorkingDir, newWorkingDir, backupDir string) error {
if err := prepareUpgradeWorkspace(path, c.file, oldWorkingDir, newWorkingDir, backupDir); err != nil {
return fmt.Errorf("prepare upgrade workspace: %w", err)
}
// copy the vars file from the old working dir to the new working dir
if err := c.file.CopyFile(filepath.Join(oldWorkingDir, terraformVarsFile), filepath.Join(newWorkingDir, terraformVarsFile)); err != nil {
return fmt.Errorf("copying vars file: %w", err)
}
return nil
}
// CreateCluster creates a Constellation cluster using Terraform. // CreateCluster creates a Constellation cluster using Terraform.
func (c *Client) CreateCluster(ctx context.Context, logLevel LogLevel) (CreateOutput, error) { func (c *Client) CreateCluster(ctx context.Context, logLevel LogLevel) (CreateOutput, error) {
if err := c.setLogLevel(logLevel); err != nil { if err := c.setLogLevel(logLevel); err != nil {

View File

@ -0,0 +1,20 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "main_lib",
srcs = ["main.go"],
importpath = "github.com/edgelesssys/constellation/v2/cli/internal/upgrade/main",
visibility = ["//visibility:private"],
deps = [
"//cli/internal/terraform",
"//cli/internal/upgrade",
"//internal/cloud/cloudprovider",
"//internal/constants",
],
)
go_binary(
name = "main",
embed = [":main_lib"],
visibility = ["//cli:__subpackages__"],
)

BIN
cli/internal/upgrade/main/main Executable file

Binary file not shown.

View File

@ -0,0 +1,35 @@
package main
import (
"bytes"
"context"
"fmt"
"path/filepath"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/cli/internal/upgrade"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
)
func main() {
ctx := context.Background()
tfClient, err := terraform.New(ctx, filepath.Join(constants.UpgradeDir, "test", constants.TerraformUpgradeWorkingDir))
if err != nil {
panic(fmt.Errorf("setting up terraform client: %w", err))
}
// give me a writer
outWriter := bytes.NewBuffer(nil)
tfUpgrader, err := upgrade.NewTerraformUpgrader(tfClient, outWriter)
if err != nil {
panic(fmt.Errorf("setting up terraform upgrader: %w", err))
}
diff, err := tfUpgrader.PlanIAMMigration(ctx, upgrade.TerraformUpgradeOptions{
CSP: cloudprovider.AWS,
LogLevel: terraform.LogLevelDebug,
}, "test")
if err != nil {
panic(fmt.Errorf("planning terraform migrations: %w", err))
}
fmt.Println(diff)
}

View File

@ -85,6 +85,31 @@ func checkFileExists(fileHandler file.Handler, existingFiles *[]string, filename
return nil return nil
} }
func (u *TerraformUpgrader) PlanIAMMigration(ctx context.Context, csp cloudprovider.Provider, logLevel terraform.LogLevel, upgradeID string) (bool, error) {
err := u.tf.PrepareIAMUpgradeWorkspace(
filepath.Join("terraform", "iam", strings.ToLower(csp.String())),
constants.TerraformIAMWorkingDir,
filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir),
filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeBackupDir),
)
if err != nil {
return false, fmt.Errorf("preparing terraform workspace: %w", err)
}
hasDiff, err := u.tf.Plan(ctx, logLevel, constants.TerraformUpgradePlanFile)
if err != nil {
return false, fmt.Errorf("terraform plan: %w", err)
}
if hasDiff {
if err := u.tf.ShowPlan(ctx, logLevel, constants.TerraformUpgradePlanFile, u.outWriter); err != nil {
return false, fmt.Errorf("terraform show plan: %w", err)
}
}
return hasDiff, nil
}
// PlanTerraformMigrations prepares the upgrade workspace and plans the Terraform migrations for the Constellation upgrade. // PlanTerraformMigrations prepares the upgrade workspace and plans the Terraform migrations for the Constellation upgrade.
// If a diff exists, it's being written to the upgrader's output writer. It also returns // If a diff exists, it's being written to the upgrader's output writer. It also returns
// a bool indicating whether a diff exists. // a bool indicating whether a diff exists.
@ -130,7 +155,7 @@ func (u *TerraformUpgrader) CleanUpTerraformMigrations(fileHandler file.Handler,
return nil return nil
} }
// ApplyTerraformMigrations applies the migerations planned by PlanTerraformMigrations. // ApplyTerraformMigrations applies the migrations planned by PlanTerraformMigrations.
// If PlanTerraformMigrations has not been executed before, it will return an error. // If PlanTerraformMigrations has not been executed before, it will return an error.
// In case of a successful upgrade, the output will be written to the specified file and the old Terraform directory is replaced // In case of a successful upgrade, the output will be written to the specified file and the old Terraform directory is replaced
// By the new one. // By the new one.
@ -176,6 +201,7 @@ func (u *TerraformUpgrader) ApplyTerraformMigrations(ctx context.Context, fileHa
// a tfClient performs the Terraform interactions in an upgrade. // a tfClient performs the Terraform interactions in an upgrade.
type tfClient interface { type tfClient interface {
PrepareIAMUpgradeWorkspace(path, oldWorkingDir, newWorkingDir, backupDir string) error
PrepareUpgradeWorkspace(path, oldWorkingDir, newWorkingDir, upgradeID string, vars terraform.Variables) error PrepareUpgradeWorkspace(path, oldWorkingDir, newWorkingDir, upgradeID string, vars terraform.Variables) error
ShowPlan(ctx context.Context, logLevel terraform.LogLevel, planFilePath string, output io.Writer) error ShowPlan(ctx context.Context, logLevel terraform.LogLevel, planFilePath string, output io.Writer) error
Plan(ctx context.Context, logLevel terraform.LogLevel, planFile string) (bool, error) Plan(ctx context.Context, logLevel terraform.LogLevel, planFile string) (bool, error)

View File

@ -154,6 +154,8 @@ const (
TerraformUpgradePlanFile = "plan.zip" TerraformUpgradePlanFile = "plan.zip"
// TerraformUpgradeWorkingDir is the directory name for the Terraform workspace being used in an upgrade. // TerraformUpgradeWorkingDir is the directory name for the Terraform workspace being used in an upgrade.
TerraformUpgradeWorkingDir = "terraform" TerraformUpgradeWorkingDir = "terraform"
// TerraformIAMUpgradeWorkingDir is the directory name for the Terraform IAM workspace being used in an upgrade.
TerraformIAMUpgradeWorkingDir = "terraform-iam"
// TerraformUpgradeBackupDir is the directory name being used to backup the pre-upgrade state in an upgrade. // TerraformUpgradeBackupDir is the directory name being used to backup the pre-upgrade state in an upgrade.
TerraformUpgradeBackupDir = "terraform-backup" TerraformUpgradeBackupDir = "terraform-backup"
// TerraformMigrationOutputFile is the file name of the output file created by a successful Terraform migration. // TerraformMigrationOutputFile is the file name of the output file created by a successful Terraform migration.