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"
|
|
|
|
|
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Client manages interaction with Terraform.
|
|
|
|
type Client struct {
|
|
|
|
tf tfInterface
|
|
|
|
|
|
|
|
file file.Handler
|
|
|
|
remove func()
|
|
|
|
}
|
|
|
|
|
|
|
|
// New sets up a new Client for Terraform.
|
2022-10-26 09:57:00 -04:00
|
|
|
func New(ctx context.Context) (*Client, error) {
|
2022-09-26 09:52:31 -04:00
|
|
|
tf, remove, err := GetExecutable(ctx, ".")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
file := file.NewHandler(afero.NewOsFs())
|
|
|
|
|
|
|
|
return &Client{
|
2022-10-26 09:57:00 -04:00
|
|
|
tf: tf,
|
|
|
|
remove: remove,
|
|
|
|
file: file,
|
2022-09-26 09:52:31 -04:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateCluster creates a Constellation cluster using Terraform.
|
2022-10-26 09:57:00 -04:00
|
|
|
func (c *Client) CreateCluster(
|
|
|
|
ctx context.Context, provider cloudprovider.Provider, name string, vars Variables,
|
|
|
|
) (string, error) {
|
|
|
|
if err := prepareWorkspace(c.file, provider); err != nil {
|
2022-10-11 06:24:33 -04:00
|
|
|
return "", err
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := c.tf.Init(ctx); err != nil {
|
2022-10-11 06:24:33 -04:00
|
|
|
return "", err
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
2022-09-27 03:22:29 -04:00
|
|
|
if err := c.file.Write(terraformVarsFile, []byte(vars.String())); err != nil {
|
2022-10-11 06:24:33 -04:00
|
|
|
return "", err
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := c.tf.Apply(ctx); err != nil {
|
2022-10-11 06:24:33 -04:00
|
|
|
return "", err
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
tfState, err := c.tf.Show(ctx)
|
|
|
|
if err != nil {
|
2022-10-11 06:24:33 -04:00
|
|
|
return "", err
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
ipOutput, ok := tfState.Values.Outputs["ip"]
|
|
|
|
if !ok {
|
2022-10-11 06:24:33 -04:00
|
|
|
return "", errors.New("no IP output found")
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
ip, ok := ipOutput.Value.(string)
|
|
|
|
if !ok {
|
2022-10-11 06:24:33 -04:00
|
|
|
return "", errors.New("invalid type in IP output: not a string")
|
2022-09-26 09:52:31 -04:00
|
|
|
}
|
|
|
|
|
2022-10-11 06:24:33 -04:00
|
|
|
return ip, 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 {
|
|
|
|
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-10-26 09:57:00 -04:00
|
|
|
if err := cleanUpWorkspace(c.file); err != nil {
|
2022-09-26 09:52:31 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := ignoreFileNotFoundErr(c.file.Remove("terraform.tfvars")); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := ignoreFileNotFoundErr(c.file.Remove("terraform.tfstate")); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := ignoreFileNotFoundErr(c.file.Remove("terraform.tfstate.backup")); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := ignoreFileNotFoundErr(c.file.Remove(".terraform.lock.hcl")); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := ignoreFileNotFoundErr(c.file.RemoveAll(".terraform")); err != nil {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|