2022-09-26 09:52:31 -04:00
|
|
|
/*
|
|
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
*/
|
|
|
|
|
|
|
|
package terraform
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
2022-11-14 12:18:58 -05:00
|
|
|
"path/filepath"
|
2022-09-26 09:52:31 -04:00
|
|
|
|
|
|
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
|
|
|
"github.com/edgelesssys/constellation/v2/internal/file"
|
|
|
|
"github.com/hashicorp/go-version"
|
|
|
|
install "github.com/hashicorp/hc-install"
|
|
|
|
"github.com/hashicorp/hc-install/fs"
|
|
|
|
"github.com/hashicorp/hc-install/product"
|
|
|
|
"github.com/hashicorp/hc-install/releases"
|
|
|
|
"github.com/hashicorp/hc-install/src"
|
|
|
|
"github.com/hashicorp/terraform-exec/tfexec"
|
|
|
|
tfjson "github.com/hashicorp/terraform-json"
|
|
|
|
"github.com/spf13/afero"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
tfVersion = ">= 1.2.0"
|
|
|
|
terraformVarsFile = "terraform.tfvars"
|
|
|
|
)
|
|
|
|
|
2022-11-16 10:33:51 -05:00
|
|
|
// ErrTerraformWorkspaceExistsWithDifferentVariables is returned when existing Terraform files differ from the version the CLI wants to extract.
|
|
|
|
var ErrTerraformWorkspaceExistsWithDifferentVariables = errors.New("creating cluster: a Terraform workspace already exists with different variables")
|
|
|
|
|
2022-09-26 09:52:31 -04:00
|
|
|
// Client manages interaction with Terraform.
|
|
|
|
type Client struct {
|
|
|
|
tf tfInterface
|
|
|
|
|
2022-11-14 12:18:58 -05:00
|
|
|
file file.Handler
|
|
|
|
workingDir string
|
|
|
|
remove func()
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// New sets up a new Client for Terraform.
|
2022-11-14 12:18:58 -05:00
|
|
|
func New(ctx context.Context, workingDir string) (*Client, error) {
|
|
|
|
file := file.NewHandler(afero.NewOsFs())
|
|
|
|
if err := file.MkdirAll(workingDir); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
tf, remove, err := GetExecutable(ctx, workingDir)
|
2022-09-26 09:52:31 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Client{
|
2022-11-14 12:18:58 -05:00
|
|
|
tf: tf,
|
|
|
|
remove: remove,
|
|
|
|
file: file,
|
|
|
|
workingDir: workingDir,
|
2022-09-26 09:52:31 -04:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2022-11-15 08:00:44 -05:00
|
|
|
// PrepareWorkspace prepares a Terraform workspace for a Constellation cluster.
|
|
|
|
func (c *Client) PrepareWorkspace(provider cloudprovider.Provider, vars Variables) error {
|
2022-11-14 12:18:58 -05:00
|
|
|
if err := prepareWorkspace(c.file, provider, c.workingDir); err != nil {
|
2022-11-15 08:00:44 -05:00
|
|
|
return err
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
2022-11-15 05:18:52 -05:00
|
|
|
if err := c.writeVars(vars); err != nil {
|
2022-11-15 08:00:44 -05:00
|
|
|
return err
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
2022-11-15 08:00:44 -05:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateCluster creates a Constellation cluster using Terraform.
|
2022-11-26 13:44:34 -05:00
|
|
|
func (c *Client) CreateCluster(ctx context.Context) (string, string, error) {
|
2022-11-15 05:18:52 -05:00
|
|
|
if err := c.tf.Init(ctx); err != nil {
|
2022-11-26 13:44:34 -05:00
|
|
|
return "", "", err
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := c.tf.Apply(ctx); err != nil {
|
2022-11-26 13:44:34 -05:00
|
|
|
return "", "", err
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
tfState, err := c.tf.Show(ctx)
|
|
|
|
if err != nil {
|
2022-11-26 13:44:34 -05:00
|
|
|
return "", "", err
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
ipOutput, ok := tfState.Values.Outputs["ip"]
|
|
|
|
if !ok {
|
2022-11-26 13:44:34 -05:00
|
|
|
return "", "", errors.New("no IP output found")
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
ip, ok := ipOutput.Value.(string)
|
|
|
|
if !ok {
|
2022-11-26 13:44:34 -05:00
|
|
|
return "", "", errors.New("invalid type in IP output: not a string")
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
2022-11-26 13:44:34 -05:00
|
|
|
secretOutput, ok := tfState.Values.Outputs["initSecret"]
|
|
|
|
if !ok {
|
|
|
|
return "", "", errors.New("no initSecret output found")
|
|
|
|
}
|
|
|
|
secret, ok := secretOutput.Value.(string)
|
|
|
|
if !ok {
|
|
|
|
return "", "", errors.New("invalid type in initSecret output: not a string")
|
|
|
|
}
|
|
|
|
|
|
|
|
return ip, secret, nil
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
2022-11-09 09:57:54 -05:00
|
|
|
// DestroyCluster destroys a Constellation cluster using Terraform.
|
2022-09-26 09:52:31 -04:00
|
|
|
func (c *Client) DestroyCluster(ctx context.Context) error {
|
2022-11-15 06:50:17 -05:00
|
|
|
if err := c.tf.Init(ctx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-09-26 09:52:31 -04:00
|
|
|
return c.tf.Destroy(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveInstaller removes the Terraform installer, if it was downloaded for this command.
|
|
|
|
func (c *Client) RemoveInstaller() {
|
|
|
|
c.remove()
|
|
|
|
}
|
|
|
|
|
|
|
|
// CleanUpWorkspace removes terraform files from the current directory.
|
|
|
|
func (c *Client) CleanUpWorkspace() error {
|
2022-11-14 12:18:58 -05:00
|
|
|
if err := cleanUpWorkspace(c.file, c.workingDir); err != nil {
|
2022-09-26 09:52:31 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetExecutable returns a Terraform executable either from the local filesystem,
|
|
|
|
// or downloads the latest version fulfilling the version constraint.
|
|
|
|
func GetExecutable(ctx context.Context, workingDir string) (terraform *tfexec.Terraform, remove func(), err error) {
|
|
|
|
inst := install.NewInstaller()
|
|
|
|
|
|
|
|
version, err := version.NewConstraint(tfVersion)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
downloadVersion := &releases.LatestVersion{
|
|
|
|
Product: product.Terraform,
|
|
|
|
Constraints: version,
|
|
|
|
}
|
|
|
|
localVersion := &fs.Version{
|
|
|
|
Product: product.Terraform,
|
|
|
|
Constraints: version,
|
|
|
|
}
|
|
|
|
|
|
|
|
execPath, err := inst.Ensure(ctx, []src.Source{localVersion, downloadVersion})
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
tf, err := tfexec.NewTerraform(workingDir, execPath)
|
|
|
|
|
|
|
|
return tf, func() { _ = inst.Remove(context.Background()) }, err
|
|
|
|
}
|
|
|
|
|
2022-11-15 05:18:52 -05:00
|
|
|
// 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 {
|
|
|
|
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) {
|
|
|
|
// 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 {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if vars.String() != string(varsContent) {
|
2022-11-16 10:33:51 -05:00
|
|
|
return ErrTerraformWorkspaceExistsWithDifferentVariables
|
2022-11-15 05:18:52 -05:00
|
|
|
}
|
|
|
|
} else if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-09-26 09:52:31 -04:00
|
|
|
type tfInterface interface {
|
|
|
|
Apply(context.Context, ...tfexec.ApplyOption) error
|
|
|
|
Destroy(context.Context, ...tfexec.DestroyOption) error
|
|
|
|
Init(context.Context, ...tfexec.InitOption) error
|
|
|
|
Show(context.Context, ...tfexec.ShowOption) (*tfjson.State, error)
|
|
|
|
}
|