mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-05-02 06:16:08 -04:00
cli: perform upgrades in-place in Terraform workspace (#2317)
* perform upgrades in-place in terraform workspace Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * update buildfiles Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add iam upgrade apply test Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * update buildfiles Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix linter Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * make config fetcher stubbable Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * change workspace restoring behaviour Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * allow overwriting existing Terraform files Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * allow overwrites of TF variables Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix iam upgrade apply Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix embed directive Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * make loader test less brittle Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * pass upgrade ID to user Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * naming nit Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * use upgradeDir Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --------- Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>
This commit is contained in:
parent
9c54ff06e0
commit
95cf4bdf21
19 changed files with 410 additions and 286 deletions
|
@ -70,6 +70,9 @@ go_library(
|
|||
"terraform/openstack/outputs.tf",
|
||||
"terraform/openstack/variables.tf",
|
||||
"terraform/qemu/modules/instance_group/tdx_domain.xsl",
|
||||
"terraform/iam/aws/.terraform.lock.hcl",
|
||||
"terraform/iam/azure/.terraform.lock.hcl",
|
||||
"terraform/iam/gcp/.terraform.lock.hcl",
|
||||
],
|
||||
importpath = "github.com/edgelesssys/constellation/v2/cli/internal/terraform",
|
||||
visibility = ["//cli:__subpackages__"],
|
||||
|
|
|
@ -25,39 +25,39 @@ var ErrTerraformWorkspaceDifferentFiles = errors.New("creating cluster: trying t
|
|||
|
||||
//go:embed terraform/*
|
||||
//go:embed terraform/*/.terraform.lock.hcl
|
||||
//go:embed terraform/iam/*/.terraform.lock.hcl
|
||||
var terraformFS embed.FS
|
||||
|
||||
const (
|
||||
noOverwrites overwritePolicy = iota
|
||||
allowOverwrites
|
||||
)
|
||||
|
||||
type overwritePolicy int
|
||||
|
||||
// prepareWorkspace loads the embedded Terraform files,
|
||||
// and writes them into the workspace.
|
||||
func prepareWorkspace(rootDir string, fileHandler file.Handler, workingDir string) error {
|
||||
return terraformCopier(fileHandler, rootDir, workingDir)
|
||||
return terraformCopier(fileHandler, rootDir, workingDir, noOverwrites)
|
||||
}
|
||||
|
||||
// prepareUpgradeWorkspace takes the Terraform state file from the old workspace and the
|
||||
// embedded Terraform files and writes them into the new workspace.
|
||||
func prepareUpgradeWorkspace(rootDir string, fileHandler file.Handler, oldWorkingDir, newWorkingDir, backupDir string) error {
|
||||
// prepareUpgradeWorkspace backs up the old Terraform workspace from workingDir, and
|
||||
// copies the embedded Terraform files into workingDir.
|
||||
func prepareUpgradeWorkspace(rootDir string, fileHandler file.Handler, workingDir, backupDir string) error {
|
||||
// backup old workspace
|
||||
if err := fileHandler.CopyDir(
|
||||
oldWorkingDir,
|
||||
workingDir,
|
||||
backupDir,
|
||||
); err != nil {
|
||||
return fmt.Errorf("backing up old workspace: %w", err)
|
||||
}
|
||||
|
||||
// copy state file
|
||||
if err := fileHandler.CopyFile(
|
||||
filepath.Join(oldWorkingDir, "terraform.tfstate"),
|
||||
filepath.Join(newWorkingDir, "terraform.tfstate"),
|
||||
file.OptMkdirAll,
|
||||
); err != nil {
|
||||
return fmt.Errorf("copying state file: %w", err)
|
||||
}
|
||||
|
||||
return terraformCopier(fileHandler, rootDir, newWorkingDir)
|
||||
return terraformCopier(fileHandler, rootDir, workingDir, allowOverwrites)
|
||||
}
|
||||
|
||||
// terraformCopier copies the embedded Terraform files into the workspace.
|
||||
func terraformCopier(fileHandler file.Handler, rootDir, workingDir string) error {
|
||||
// allowOverwrites allows overwriting existing files in the workspace.
|
||||
func terraformCopier(fileHandler file.Handler, rootDir, workingDir string, overwritePolicy overwritePolicy) error {
|
||||
goEmbedRootDir := filepath.ToSlash(rootDir)
|
||||
return fs.WalkDir(terraformFS, goEmbedRootDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
|
@ -74,8 +74,15 @@ func terraformCopier(fileHandler file.Handler, rootDir, workingDir string) error
|
|||
}
|
||||
// normalize
|
||||
fileName := strings.Replace(slashpath.Join(workingDir, path), goEmbedRootDir+"/", "", 1)
|
||||
if err := fileHandler.Write(fileName, content, file.OptMkdirAll); errors.Is(err, afero.ErrFileExists) {
|
||||
// If a file already exists, check if it is identical. If yes, continue and don't write anything to disk.
|
||||
opts := []file.Option{
|
||||
file.OptMkdirAll,
|
||||
}
|
||||
if overwritePolicy == allowOverwrites {
|
||||
opts = append(opts, file.OptOverwrite)
|
||||
}
|
||||
if err := fileHandler.Write(fileName, content, opts...); errors.Is(err, afero.ErrFileExists) {
|
||||
// If a file already exists and overwritePolicy is set to noOverwrites,
|
||||
// check if it is identical. If yes, continue and don't write anything to disk.
|
||||
// If no, don't overwrite it and instead throw an error. The affected file could be from a different version,
|
||||
// provider, corrupted or manually modified in general.
|
||||
existingFileContent, err := fileHandler.Read(fileName)
|
||||
|
|
|
@ -20,6 +20,8 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var oldFileContent = []byte("1234")
|
||||
|
||||
func TestPrepareWorkspace(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
pathBase string
|
||||
|
@ -109,20 +111,20 @@ func TestPrepareWorkspace(t *testing.T) {
|
|||
err := prepareWorkspace(path, file, testWorkspace)
|
||||
|
||||
require.NoError(err)
|
||||
checkFiles(t, file, func(err error) { assert.NoError(err) }, testWorkspace, tc.fileList)
|
||||
checkFiles(t, file, func(err error) { assert.NoError(err) }, nil, testWorkspace, tc.fileList)
|
||||
|
||||
if tc.testAlreadyUnpacked {
|
||||
// Let's try the same again and check if we don't get a "file already exists" error.
|
||||
require.NoError(file.Remove(filepath.Join(testWorkspace, "variables.tf")))
|
||||
err := prepareWorkspace(path, file, testWorkspace)
|
||||
assert.NoError(err)
|
||||
checkFiles(t, file, func(err error) { assert.NoError(err) }, testWorkspace, tc.fileList)
|
||||
checkFiles(t, file, func(err error) { assert.NoError(err) }, nil, testWorkspace, tc.fileList)
|
||||
}
|
||||
|
||||
err = cleanUpWorkspace(file, testWorkspace)
|
||||
require.NoError(err)
|
||||
|
||||
checkFiles(t, file, func(err error) { assert.ErrorIs(err, fs.ErrNotExist) }, testWorkspace, tc.fileList)
|
||||
checkFiles(t, file, func(err error) { assert.ErrorIs(err, fs.ErrNotExist) }, nil, testWorkspace, tc.fileList)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -131,49 +133,56 @@ func TestPrepareUpgradeWorkspace(t *testing.T) {
|
|||
testCases := map[string]struct {
|
||||
pathBase string
|
||||
provider cloudprovider.Provider
|
||||
oldWorkingDir string
|
||||
newWorkingDir string
|
||||
workingDir string
|
||||
backupDir string
|
||||
oldWorkspaceFiles []string
|
||||
newWorkspaceFiles []string
|
||||
workspaceFiles []string
|
||||
expectedFiles []string
|
||||
expectedBackupFiles []string
|
||||
testAlreadyUnpacked bool
|
||||
wantErr bool
|
||||
}{
|
||||
"works": {
|
||||
pathBase: "terraform",
|
||||
provider: cloudprovider.AWS,
|
||||
oldWorkingDir: "old",
|
||||
newWorkingDir: "new",
|
||||
backupDir: "backup",
|
||||
oldWorkspaceFiles: []string{"terraform.tfstate"},
|
||||
pathBase: "terraform",
|
||||
provider: cloudprovider.AWS,
|
||||
workingDir: "working",
|
||||
backupDir: "backup",
|
||||
workspaceFiles: []string{"main.tf", "variables.tf", "outputs.tf"},
|
||||
expectedFiles: []string{
|
||||
"main.tf",
|
||||
"variables.tf",
|
||||
"outputs.tf",
|
||||
"modules",
|
||||
"terraform.tfstate",
|
||||
},
|
||||
expectedBackupFiles: []string{
|
||||
"main.tf",
|
||||
"variables.tf",
|
||||
"outputs.tf",
|
||||
},
|
||||
},
|
||||
"state file does not exist": {
|
||||
pathBase: "terraform",
|
||||
provider: cloudprovider.AWS,
|
||||
oldWorkingDir: "old",
|
||||
newWorkingDir: "new",
|
||||
backupDir: "backup",
|
||||
oldWorkspaceFiles: []string{},
|
||||
expectedFiles: []string{},
|
||||
wantErr: true,
|
||||
pathBase: "terraform",
|
||||
provider: cloudprovider.AWS,
|
||||
workingDir: "working",
|
||||
backupDir: "backup",
|
||||
workspaceFiles: []string{},
|
||||
expectedFiles: []string{},
|
||||
wantErr: true,
|
||||
},
|
||||
"terraform files already exist in new dir": {
|
||||
pathBase: "terraform",
|
||||
provider: cloudprovider.AWS,
|
||||
oldWorkingDir: "old",
|
||||
newWorkingDir: "new",
|
||||
backupDir: "backup",
|
||||
oldWorkspaceFiles: []string{"terraform.tfstate"},
|
||||
newWorkspaceFiles: []string{"main.tf"},
|
||||
wantErr: true,
|
||||
"terraform file already exists in working dir (overwrite)": {
|
||||
pathBase: "terraform",
|
||||
provider: cloudprovider.AWS,
|
||||
workingDir: "working",
|
||||
backupDir: "backup",
|
||||
workspaceFiles: []string{"main.tf", "variables.tf", "outputs.tf"},
|
||||
expectedFiles: []string{
|
||||
"main.tf",
|
||||
"variables.tf",
|
||||
"outputs.tf",
|
||||
},
|
||||
expectedBackupFiles: []string{
|
||||
"main.tf",
|
||||
"variables.tf",
|
||||
"outputs.tf",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -186,31 +195,44 @@ func TestPrepareUpgradeWorkspace(t *testing.T) {
|
|||
|
||||
path := path.Join(tc.pathBase, strings.ToLower(tc.provider.String()))
|
||||
|
||||
createFiles(t, file, tc.oldWorkspaceFiles, tc.oldWorkingDir)
|
||||
createFiles(t, file, tc.newWorkspaceFiles, tc.newWorkingDir)
|
||||
createFiles(t, file, tc.workspaceFiles, tc.workingDir)
|
||||
|
||||
err := prepareUpgradeWorkspace(path, file, tc.oldWorkingDir, tc.newWorkingDir, tc.backupDir)
|
||||
err := prepareUpgradeWorkspace(path, file, tc.workingDir, tc.backupDir)
|
||||
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
} else {
|
||||
require.NoError(err)
|
||||
checkFiles(
|
||||
t, file,
|
||||
func(err error) { assert.NoError(err) },
|
||||
func(content []byte) { assert.NotEqual(oldFileContent, content) },
|
||||
tc.workingDir, tc.expectedFiles,
|
||||
)
|
||||
checkFiles(
|
||||
t, file,
|
||||
func(err error) { assert.NoError(err) },
|
||||
func(content []byte) { assert.Equal(oldFileContent, content) },
|
||||
tc.backupDir, tc.expectedBackupFiles,
|
||||
)
|
||||
}
|
||||
checkFiles(t, file, func(err error) { assert.NoError(err) }, tc.newWorkingDir, tc.expectedFiles)
|
||||
checkFiles(t, file, func(err error) { assert.NoError(err) },
|
||||
tc.backupDir,
|
||||
tc.oldWorkspaceFiles,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkFiles(t *testing.T, fileHandler file.Handler, assertion func(error), dir string, files []string) {
|
||||
func checkFiles(t *testing.T, fileHandler file.Handler, assertion func(error), contentExpection func(content []byte), dir string, files []string) {
|
||||
t.Helper()
|
||||
for _, f := range files {
|
||||
path := filepath.Join(dir, f)
|
||||
_, err := fileHandler.Stat(path)
|
||||
assertion(err)
|
||||
if err == nil {
|
||||
content, err := fileHandler.Read(path)
|
||||
assertion(err)
|
||||
if contentExpection != nil {
|
||||
contentExpection(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,7 +242,7 @@ func createFiles(t *testing.T, fileHandler file.Handler, fileList []string, targ
|
|||
|
||||
for _, f := range fileList {
|
||||
path := filepath.Join(targetDir, f)
|
||||
err := fileHandler.Write(path, []byte("1234"), file.OptOverwrite, file.OptMkdirAll)
|
||||
err := fileHandler.Write(path, oldFileContent, file.OptOverwrite, file.OptMkdirAll)
|
||||
require.NoError(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -332,19 +332,18 @@ func (c *Client) PrepareWorkspace(path string, vars Variables) error {
|
|||
return fmt.Errorf("prepare workspace: %w", err)
|
||||
}
|
||||
|
||||
return c.writeVars(vars)
|
||||
return c.writeVars(vars, noOverwrites)
|
||||
}
|
||||
|
||||
// PrepareUpgradeWorkspace prepares a Terraform workspace for an upgrade.
|
||||
// It copies the Terraform state from the old working dir and the embedded Terraform files
|
||||
// into the working dir of the Terraform client.
|
||||
// Additionally, a backup of the old working dir is created in the backup dir.
|
||||
func (c *Client) PrepareUpgradeWorkspace(path, oldWorkingDir, backupDir string, vars Variables) error {
|
||||
if err := prepareUpgradeWorkspace(path, c.file, oldWorkingDir, c.workingDir, backupDir); err != nil {
|
||||
// It creates a backup of the Terraform workspace in the backupDir, and copies
|
||||
// the embedded Terraform files into the workingDir.
|
||||
func (c *Client) PrepareUpgradeWorkspace(path, backupDir string, vars Variables) error {
|
||||
if err := prepareUpgradeWorkspace(path, c.file, c.workingDir, backupDir); err != nil {
|
||||
return fmt.Errorf("prepare upgrade workspace: %w", err)
|
||||
}
|
||||
|
||||
return c.writeVars(vars)
|
||||
return c.writeVars(vars, allowOverwrites)
|
||||
}
|
||||
|
||||
// ApplyCluster applies the Terraform configuration of the workspace to create or upgrade a Constellation cluster.
|
||||
|
@ -461,13 +460,17 @@ func (c *Client) applyManualStateMigrations(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// writeVars tries to write the Terraform variables file or, if it exists, checks if it is the same as we are expecting.
|
||||
func (c *Client) writeVars(vars Variables) error {
|
||||
func (c *Client) writeVars(vars Variables, overwritePolicy overwritePolicy) error {
|
||||
if vars == nil {
|
||||
return errors.New("creating cluster: vars is nil")
|
||||
}
|
||||
|
||||
pathToVarsFile := filepath.Join(c.workingDir, terraformVarsFile)
|
||||
if err := c.file.Write(pathToVarsFile, []byte(vars.String())); errors.Is(err, afero.ErrFileExists) {
|
||||
opts := []file.Option{}
|
||||
if overwritePolicy == allowOverwrites {
|
||||
opts = append(opts, file.OptOverwrite)
|
||||
}
|
||||
if err := c.file.Write(pathToVarsFile, []byte(vars.String()), opts...); errors.Is(err, afero.ErrFileExists) {
|
||||
// If a variables file already exists, check if it's the same as we're expecting, so we can continue using it.
|
||||
varsContent, err := c.file.Read(pathToVarsFile)
|
||||
if err != nil {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue