2022-09-26 09:52:31 -04:00
/ *
Copyright ( c ) Edgeless Systems GmbH
SPDX - License - Identifier : AGPL - 3.0 - only
* /
2023-01-19 09:57:50 -05:00
/ *
2023-08-08 09:18:36 -04:00
Package terraform handles creation / destruction of cloud and IAM resources required by Constellation using Terraform .
2023-01-19 09:57:50 -05:00
Since Terraform does not provide a stable Go API , we use the ` terraform-exec ` package to interact with Terraform .
2023-10-26 04:55:50 -04:00
The Terraform templates are located in the constants . TerraformEmbeddedDir subdirectory . The templates are embedded into the CLI binary using ` go:embed ` .
2023-01-19 09:57:50 -05:00
On use the relevant template is extracted to the working directory and the user customized variables are written to a ` terraform.tfvars ` file .
2023-08-08 09:18:36 -04:00
Functions in this package should be kept CSP agnostic ( there should be no "CreateAzureCluster" function ) ,
as loading the correct values and calling the correct functions for a given CSP is handled by the ` cloudcmd ` package .
2023-01-19 09:57:50 -05:00
* /
2022-09-26 09:52:31 -04:00
package terraform
import (
"context"
"errors"
2023-04-14 08:15:07 -04:00
"fmt"
2023-05-22 07:31:20 -04:00
"io"
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"
2023-04-14 08:15:07 -04:00
"github.com/edgelesssys/constellation/v2/internal/constants"
2023-12-08 10:27:04 -05:00
"github.com/edgelesssys/constellation/v2/internal/constellation/state"
2022-09-26 09:52:31 -04:00
"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 (
2023-08-16 17:25:53 -04:00
// Enforce "<1.6.0" to ensure that only MPL licensed Terraform versions are used.
tfVersion = ">= 1.4.6, < 1.6.0"
2022-09-26 09:52:31 -04:00
terraformVarsFile = "terraform.tfvars"
2023-08-04 07:53:51 -04:00
// terraformUpgradePlanFile is the file name of the zipfile created by Terraform plan for Constellation upgrades.
terraformUpgradePlanFile = "plan.zip"
2022-09-26 09:52:31 -04:00
)
// Client manages interaction with Terraform.
type Client struct {
tf tfInterface
2023-06-27 07:12:50 -04:00
manualStateMigrations [ ] StateMigration
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
}
2023-08-21 04:26:53 -04:00
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
}
2023-06-27 07:12:50 -04:00
// WithManualStateMigration adds a manual state migration to the Client.
func ( c * Client ) WithManualStateMigration ( migration StateMigration ) * Client {
c . manualStateMigrations = append ( c . manualStateMigrations , migration )
return c
}
2023-07-31 04:53:05 -04:00
// ShowIAM reads the state of Constellation IAM resources from Terraform.
func ( c * Client ) ShowIAM ( ctx context . Context , provider cloudprovider . Provider ) ( IAMOutput , error ) {
tfState , err := c . tf . Show ( ctx )
if err != nil {
return IAMOutput { } , err
2022-09-26 09:52:31 -04:00
}
2023-08-09 09:25:59 -04:00
if tfState == nil || tfState . Values == nil {
return IAMOutput { } , errors . New ( "terraform show: no values returned" )
}
2022-09-26 09:52:31 -04:00
2023-07-31 04:53:05 -04:00
switch provider {
case cloudprovider . GCP :
2023-12-15 04:36:58 -05:00
saKeyOutputRaw , ok := tfState . Values . Outputs [ "service_account_key" ]
2023-07-31 04:53:05 -04:00
if ! ok {
2023-12-15 04:36:58 -05:00
return IAMOutput { } , errors . New ( "no service_account_key output found" )
2023-07-31 04:53:05 -04:00
}
saKeyOutput , ok := saKeyOutputRaw . Value . ( string )
if ! ok {
2023-12-15 04:36:58 -05:00
return IAMOutput { } , errors . New ( "invalid type in service_account_key output: not a string" )
2023-07-31 04:53:05 -04:00
}
return IAMOutput {
GCP : GCPIAMOutput {
SaKey : saKeyOutput ,
} ,
} , nil
case cloudprovider . Azure :
subscriptionIDRaw , ok := tfState . Values . Outputs [ "subscription_id" ]
if ! ok {
2023-12-15 04:36:58 -05:00
return IAMOutput { } , errors . New ( "no subscription_id output found" )
2023-07-31 04:53:05 -04:00
}
subscriptionIDOutput , ok := subscriptionIDRaw . Value . ( string )
if ! ok {
2023-12-15 04:36:58 -05:00
return IAMOutput { } , errors . New ( "invalid type in subscription_id output: not a string" )
2023-07-31 04:53:05 -04:00
}
tenantIDRaw , ok := tfState . Values . Outputs [ "tenant_id" ]
if ! ok {
2023-12-15 04:36:58 -05:00
return IAMOutput { } , errors . New ( "no tenant_id output found" )
2023-07-31 04:53:05 -04:00
}
tenantIDOutput , ok := tenantIDRaw . Value . ( string )
if ! ok {
2023-12-15 04:36:58 -05:00
return IAMOutput { } , errors . New ( "invalid type in tenant_id output: not a string" )
2023-07-31 04:53:05 -04:00
}
uamiIDRaw , ok := tfState . Values . Outputs [ "uami_id" ]
if ! ok {
2023-12-15 04:36:58 -05:00
return IAMOutput { } , errors . New ( "no uami_id output found" )
2023-07-31 04:53:05 -04:00
}
uamiIDOutput , ok := uamiIDRaw . Value . ( string )
if ! ok {
2023-12-15 04:36:58 -05:00
return IAMOutput { } , errors . New ( "invalid type in uami_id output: not a string" )
2023-07-31 04:53:05 -04:00
}
return IAMOutput {
Azure : AzureIAMOutput {
SubscriptionID : subscriptionIDOutput ,
TenantID : tenantIDOutput ,
UAMIID : uamiIDOutput ,
} ,
} , nil
case cloudprovider . AWS :
2023-12-15 04:36:58 -05:00
controlPlaneProfileRaw , ok := tfState . Values . Outputs [ "iam_instance_profile_name_control_plane" ]
2023-07-31 04:53:05 -04:00
if ! ok {
2023-12-15 04:36:58 -05:00
return IAMOutput { } , errors . New ( "no iam_instance_profile_name_control_plane output found" )
2023-07-31 04:53:05 -04:00
}
controlPlaneProfileOutput , ok := controlPlaneProfileRaw . Value . ( string )
if ! ok {
2023-12-15 04:36:58 -05:00
return IAMOutput { } , errors . New ( "invalid type in iam_instance_profile_name_control_plane output: not a string" )
2023-07-31 04:53:05 -04:00
}
2023-12-15 04:36:58 -05:00
workerNodeProfileRaw , ok := tfState . Values . Outputs [ "iam_instance_profile_name_worker_nodes" ]
2023-07-31 04:53:05 -04:00
if ! ok {
2023-12-15 04:36:58 -05:00
return IAMOutput { } , errors . New ( "no iam_instance_profile_name_worker_nodes output found" )
2023-07-31 04:53:05 -04:00
}
workerNodeProfileOutput , ok := workerNodeProfileRaw . Value . ( string )
if ! ok {
2023-12-15 04:36:58 -05:00
return IAMOutput { } , errors . New ( "invalid type in iam_instance_profile_name_worker_nodes output: not a string" )
2023-07-31 04:53:05 -04:00
}
return IAMOutput {
AWS : AWSIAMOutput {
ControlPlaneInstanceProfile : controlPlaneProfileOutput ,
WorkerNodeInstanceProfile : workerNodeProfileOutput ,
} ,
} , nil
default :
return IAMOutput { } , errors . New ( "unsupported cloud provider" )
2023-07-24 04:30:53 -04:00
}
}
2023-09-25 10:19:43 -04:00
// ShowInfrastructure reads the state of Constellation cluster resources from Terraform.
func ( c * Client ) ShowInfrastructure ( ctx context . Context , provider cloudprovider . Provider ) ( state . Infrastructure , error ) {
2022-09-26 09:52:31 -04:00
tfState , err := c . tf . Show ( ctx )
if err != nil {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , fmt . Errorf ( "terraform show: %w" , err )
2022-09-26 09:52:31 -04:00
}
2023-08-03 07:54:48 -04:00
if tfState . Values == nil {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "terraform show: no values returned" )
2023-08-03 07:54:48 -04:00
}
2022-09-26 09:52:31 -04:00
2023-10-17 09:46:15 -04:00
outOfClusterEndpointOutput , ok := tfState . Values . Outputs [ "out_of_cluster_endpoint" ]
2022-09-26 09:52:31 -04:00
if ! ok {
2023-10-17 09:46:15 -04:00
return state . Infrastructure { } , errors . New ( "no out_of_cluster_endpoint output found" )
2022-09-26 09:52:31 -04:00
}
2023-10-17 09:46:15 -04:00
outOfClusterEndpoint , ok := outOfClusterEndpointOutput . Value . ( string )
if ! ok {
return state . Infrastructure { } , errors . New ( "invalid type in IP output: not a string" )
}
inClusterEndpointOutput , ok := tfState . Values . Outputs [ "in_cluster_endpoint" ]
if ! ok {
return state . Infrastructure { } , errors . New ( "no in_cluster_endpoint output found" )
}
inClusterEndpoint , ok := inClusterEndpointOutput . Value . ( string )
2022-09-26 09:52:31 -04:00
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "invalid type in IP output: not a string" )
2023-07-21 10:43:51 -04:00
}
apiServerCertSANsOutput , ok := tfState . Values . Outputs [ "api_server_cert_sans" ]
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "no api_server_cert_sans output found" )
2023-07-21 10:43:51 -04:00
}
apiServerCertSANsUntyped , ok := apiServerCertSANsOutput . Value . ( [ ] any )
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , fmt . Errorf ( "invalid type in api_server_cert_sans output: %s is not a list of elements" , apiServerCertSANsOutput . Type . FriendlyName ( ) )
2023-07-21 10:43:51 -04:00
}
apiServerCertSANs , err := toStringSlice ( apiServerCertSANsUntyped )
if err != nil {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , fmt . Errorf ( "convert api_server_cert_sans output: %w" , err )
2022-09-26 09:52:31 -04:00
}
2023-12-15 04:36:58 -05:00
secretOutput , ok := tfState . Values . Outputs [ "init_secret" ]
2022-11-26 13:44:34 -05:00
if ! ok {
2023-12-15 04:36:58 -05:00
return state . Infrastructure { } , errors . New ( "no init_secret output found" )
2022-11-26 13:44:34 -05:00
}
secret , ok := secretOutput . Value . ( string )
if ! ok {
2023-12-15 04:36:58 -05:00
return state . Infrastructure { } , errors . New ( "invalid type in init_Secret output: not a string" )
2022-11-26 13:44:34 -05:00
}
2023-01-19 04:41:07 -05:00
uidOutput , ok := tfState . Values . Outputs [ "uid" ]
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "no uid output found" )
2023-01-19 04:41:07 -05:00
}
uid , ok := uidOutput . Value . ( string )
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "invalid type in uid output: not a string" )
2023-01-19 04:41:07 -05:00
}
2023-10-09 07:04:29 -04:00
nameOutput , ok := tfState . Values . Outputs [ "name" ]
if ! ok {
return state . Infrastructure { } , errors . New ( "no name output found" )
}
name , ok := nameOutput . Value . ( string )
if ! ok {
return state . Infrastructure { } , errors . New ( "invalid type in name output: not a string" )
}
2023-12-15 04:36:58 -05:00
cidrNodesOutput , ok := tfState . Values . Outputs [ "ip_cidr_node" ]
2023-10-23 09:06:48 -04:00
if ! ok {
2023-12-15 04:36:58 -05:00
return state . Infrastructure { } , errors . New ( "no ip_cidr_node output found" )
2023-10-23 09:06:48 -04:00
}
cidrNodes , ok := cidrNodesOutput . Value . ( string )
if ! ok {
2023-12-15 04:36:58 -05:00
return state . Infrastructure { } , errors . New ( "invalid type in ip_cidr_node output: not a string" )
2023-10-23 09:06:48 -04:00
}
2023-09-25 11:10:23 -04:00
res := state . Infrastructure {
2023-10-17 09:46:15 -04:00
ClusterEndpoint : outOfClusterEndpoint ,
InClusterEndpoint : inClusterEndpoint ,
2023-07-21 10:43:51 -04:00
APIServerCertSANs : apiServerCertSANs ,
2023-10-09 07:04:29 -04:00
InitSecret : [ ] byte ( secret ) ,
2023-07-21 10:43:51 -04:00
UID : uid ,
2023-10-09 07:04:29 -04:00
Name : name ,
2023-10-23 09:06:48 -04:00
IPCidrNode : cidrNodes ,
2023-07-31 04:53:05 -04:00
}
2023-08-03 10:17:23 -04:00
2023-07-31 04:53:05 -04:00
switch provider {
case cloudprovider . GCP :
gcpProjectOutput , ok := tfState . Values . Outputs [ "project" ]
2023-08-03 10:17:23 -04:00
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "no project output found" )
2023-08-03 10:17:23 -04:00
}
gcpProject , ok := gcpProjectOutput . Value . ( string )
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "invalid type in project output: not a string" )
2023-08-03 10:17:23 -04:00
}
2023-12-15 04:36:58 -05:00
cidrPodsOutput , ok := tfState . Values . Outputs [ "ip_cidr_pod" ]
2023-08-03 10:17:23 -04:00
if ! ok {
2023-12-15 04:36:58 -05:00
return state . Infrastructure { } , errors . New ( "no ip_cidr_pod output found" )
2023-08-03 10:17:23 -04:00
}
cidrPods , ok := cidrPodsOutput . Value . ( string )
if ! ok {
2023-12-15 04:36:58 -05:00
return state . Infrastructure { } , errors . New ( "invalid type in ip_cidr_pod output: not a string" )
2023-08-03 10:17:23 -04:00
}
2023-09-25 11:10:23 -04:00
res . GCP = & state . GCP {
2023-10-23 09:06:48 -04:00
ProjectID : gcpProject ,
IPCidrPod : cidrPods ,
2023-07-31 04:53:05 -04:00
}
case cloudprovider . Azure :
2023-12-15 04:36:58 -05:00
attestationURLOutput , ok := tfState . Values . Outputs [ "attestation_url" ]
2023-08-03 10:17:23 -04:00
if ! ok {
2023-12-15 04:36:58 -05:00
return state . Infrastructure { } , errors . New ( "no attestation_url output found" )
2023-08-03 10:17:23 -04:00
}
attestationURL , ok := attestationURLOutput . Value . ( string )
if ! ok {
2023-12-15 04:36:58 -05:00
return state . Infrastructure { } , errors . New ( "invalid type in attestation_url output: not a string" )
2023-07-31 04:53:05 -04:00
}
2023-08-01 02:40:44 -04:00
azureUAMIOutput , ok := tfState . Values . Outputs [ "user_assigned_identity_client_id" ]
2023-07-31 04:53:05 -04:00
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "no user_assigned_identity_client_id output found" )
2023-07-31 04:53:05 -04:00
}
azureUAMI , ok := azureUAMIOutput . Value . ( string )
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "invalid type in user_assigned_identity_client_id output: not a string" )
2023-07-31 04:53:05 -04:00
}
rgOutput , ok := tfState . Values . Outputs [ "resource_group" ]
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "no resource_group output found" )
2023-07-31 04:53:05 -04:00
}
rg , ok := rgOutput . Value . ( string )
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "invalid type in resource_group output: not a string" )
2023-07-31 04:53:05 -04:00
}
subscriptionOutput , ok := tfState . Values . Outputs [ "subscription_id" ]
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "no subscription_id output found" )
2023-07-31 04:53:05 -04:00
}
subscriptionID , ok := subscriptionOutput . Value . ( string )
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "invalid type in subscription_id output: not a string" )
2023-07-31 04:53:05 -04:00
}
networkSGNameOutput , ok := tfState . Values . Outputs [ "network_security_group_name" ]
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "no network_security_group_name output found" )
2023-07-31 04:53:05 -04:00
}
networkSGName , ok := networkSGNameOutput . Value . ( string )
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "invalid type in network_security_group_name output: not a string" )
2023-07-31 04:53:05 -04:00
}
loadBalancerNameOutput , ok := tfState . Values . Outputs [ "loadbalancer_name" ]
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "no loadbalancer_name output found" )
2023-07-31 04:53:05 -04:00
}
loadBalancerName , ok := loadBalancerNameOutput . Value . ( string )
if ! ok {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , errors . New ( "invalid type in loadbalancer_name output: not a string" )
2023-07-31 04:53:05 -04:00
}
2023-09-25 11:10:23 -04:00
res . Azure = & state . Azure {
2023-07-31 04:53:05 -04:00
ResourceGroup : rg ,
SubscriptionID : subscriptionID ,
UserAssignedIdentity : azureUAMI ,
NetworkSecurityGroupName : networkSGName ,
LoadBalancerName : loadBalancerName ,
AttestationURL : attestationURL ,
}
2024-02-09 11:27:12 -05:00
case cloudprovider . OpenStack :
networkIDOutput , ok := tfState . Values . Outputs [ "network_id" ]
if ! ok {
return state . Infrastructure { } , errors . New ( "no network_id output found" )
}
networkID , ok := networkIDOutput . Value . ( string )
if ! ok {
return state . Infrastructure { } , errors . New ( "invalid type in network_id output: not a string" )
}
res . OpenStack = & state . OpenStack {
NetworkID : networkID ,
}
2023-07-31 04:53:05 -04:00
}
return res , nil
}
// PrepareWorkspace prepares a Terraform workspace for a Constellation cluster.
func ( c * Client ) PrepareWorkspace ( path string , vars Variables ) error {
if err := prepareWorkspace ( path , c . file , c . workingDir ) ; err != nil {
return fmt . Errorf ( "prepare workspace: %w" , err )
}
2023-10-26 04:55:50 -04:00
return c . writeVars ( vars )
2023-07-31 04:53:05 -04:00
}
2023-08-21 04:26:53 -04:00
// ApplyCluster applies the Terraform configuration of the workspace to create or upgrade a Constellation cluster.
2023-09-25 11:10:23 -04:00
func ( c * Client ) ApplyCluster ( ctx context . Context , provider cloudprovider . Provider , logLevel LogLevel ) ( state . Infrastructure , error ) {
2023-08-21 04:26:53 -04:00
if err := c . apply ( ctx , logLevel ) ; err != nil {
2023-09-25 11:10:23 -04:00
return state . Infrastructure { } , err
2023-07-31 04:53:05 -04:00
}
2023-09-25 11:10:23 -04:00
return c . ShowInfrastructure ( ctx , provider )
2023-01-19 04:41:07 -05:00
}
2023-08-21 04:26:53 -04:00
// ApplyIAM applies the Terraform configuration of the workspace to create or upgrade an IAM configuration.
func ( c * Client ) ApplyIAM ( ctx context . Context , provider cloudprovider . Provider , logLevel LogLevel ) ( IAMOutput , error ) {
if err := c . apply ( ctx , logLevel ) ; err != nil {
2022-12-07 05:48:54 -05:00
return IAMOutput { } , err
}
2023-07-31 04:53:05 -04:00
return c . ShowIAM ( ctx , provider )
2022-12-07 05:48:54 -05:00
}
2023-08-04 07:53:51 -04:00
// Plan determines the diff that will be applied by Terraform.
// The plan output is written to the Terraform working directory.
2023-05-22 07:31:20 -04:00
// If there is a diff, the returned bool is true. Otherwise, it is false.
2023-08-04 07:53:51 -04:00
func ( c * Client ) Plan ( ctx context . Context , logLevel LogLevel ) ( bool , error ) {
2023-05-22 07:31:20 -04:00
if err := c . setLogLevel ( logLevel ) ; err != nil {
return false , fmt . Errorf ( "set terraform log level %s: %w" , logLevel . String ( ) , err )
}
if err := c . tf . Init ( ctx ) ; err != nil {
return false , fmt . Errorf ( "terraform init: %w" , err )
}
2023-06-27 07:12:50 -04:00
if err := c . applyManualStateMigrations ( ctx ) ; err != nil {
return false , fmt . Errorf ( "apply manual state migrations: %w" , err )
}
2023-05-22 07:31:20 -04:00
opts := [ ] tfexec . PlanOption {
2023-08-04 07:53:51 -04:00
tfexec . Out ( terraformUpgradePlanFile ) ,
2023-05-22 07:31:20 -04:00
}
return c . tf . Plan ( ctx , opts ... )
}
2023-08-04 07:53:51 -04:00
// ShowPlan formats the diff of a plan file in the Terraform working directory,
// and writes it to the specified output.
func ( c * Client ) ShowPlan ( ctx context . Context , logLevel LogLevel , output io . Writer ) error {
2023-05-22 07:31:20 -04:00
if err := c . setLogLevel ( logLevel ) ; err != nil {
return fmt . Errorf ( "set terraform log level %s: %w" , logLevel . String ( ) , err )
}
2023-08-04 07:53:51 -04:00
planResult , err := c . tf . ShowPlanFileRaw ( ctx , terraformUpgradePlanFile )
2023-05-22 07:31:20 -04:00
if err != nil {
return fmt . Errorf ( "terraform show plan: %w" , err )
}
_ , err = output . Write ( [ ] byte ( planResult ) )
if err != nil {
return fmt . Errorf ( "write plan output: %w" , err )
}
return nil
}
2023-02-13 02:42:54 -05:00
// Destroy destroys Terraform-created cloud resources.
2023-04-14 08:15:07 -04:00
func ( c * Client ) Destroy ( ctx context . Context , logLevel LogLevel ) error {
if err := c . setLogLevel ( logLevel ) ; err != nil {
return fmt . Errorf ( "set terraform log level %s: %w" , logLevel . String ( ) , err )
}
2022-11-15 06:50:17 -05:00
if err := c . tf . Init ( ctx ) ; err != nil {
2023-04-14 08:15:07 -04:00
return fmt . Errorf ( "terraform init: %w" , err )
2022-11-15 06:50:17 -05:00
}
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 {
2023-03-20 06:06:51 -04:00
return cleanUpWorkspace ( c . file , c . workingDir )
2022-09-26 09:52:31 -04:00
}
2023-08-21 04:26:53 -04:00
func ( c * Client ) apply ( ctx context . Context , logLevel LogLevel ) error {
if err := c . setLogLevel ( logLevel ) ; err != nil {
return fmt . Errorf ( "set terraform log level %s: %w" , logLevel . String ( ) , err )
2022-09-26 09:52:31 -04:00
}
2023-08-21 04:26:53 -04:00
if err := c . tf . Init ( ctx ) ; err != nil {
return fmt . Errorf ( "terraform init: %w" , err )
2023-08-16 17:25:53 -04:00
}
2023-08-21 04:26:53 -04:00
if err := c . applyManualStateMigrations ( ctx ) ; err != nil {
return fmt . Errorf ( "apply manual state migrations: %w" , err )
2022-09-26 09:52:31 -04:00
}
2023-08-21 04:26:53 -04:00
if err := c . tf . Apply ( ctx ) ; err != nil {
return fmt . Errorf ( "terraform apply: %w" , err )
2022-09-26 09:52:31 -04:00
}
2023-08-21 04:26:53 -04:00
return nil
2022-09-26 09:52:31 -04:00
}
2023-06-27 07:12:50 -04:00
// applyManualStateMigrations applies manual state migrations that are not handled by Terraform due to missing features.
2023-08-21 04:26:53 -04:00
// This functions expects to be run on an initialized Terraform workspace.
2023-06-27 07:12:50 -04:00
// Each migration is expected to be idempotent.
// This is a temporary solution until we can remove the need for manual state migrations.
func ( c * Client ) applyManualStateMigrations ( ctx context . Context ) error {
for _ , migration := range c . manualStateMigrations {
if err := migration . Hook ( ctx , c . tf ) ; err != nil {
return fmt . Errorf ( "apply manual state migration %s: %w" , migration . DisplayName , err )
}
}
return nil
}
2023-10-26 04:55:50 -04:00
// writeVars writes / overwrites the Terraform variables file.
func ( c * Client ) writeVars ( vars Variables ) error {
2022-11-15 05:18:52 -05:00
if vars == nil {
return errors . New ( "creating cluster: vars is nil" )
}
pathToVarsFile := filepath . Join ( c . workingDir , terraformVarsFile )
2023-10-26 04:55:50 -04:00
// Allow overwriting existing files.
// If we are creating a new cluster, the workspace must have been empty before,
// so there is no risk of overwriting existing files.
// If we are upgrading an existing cluster, we want to overwrite the existing files,
// and we have already created a backup of the existing workspace.
if err := c . file . Write ( pathToVarsFile , [ ] byte ( vars . String ( ) ) , file . OptOverwrite ) ; err != nil {
2023-05-23 04:49:47 -04:00
return fmt . Errorf ( "write variables file: %w" , err )
2022-11-15 05:18:52 -05:00
}
return nil
}
2023-04-14 08:15:07 -04:00
// setLogLevel sets the log level for Terraform.
func ( c * Client ) setLogLevel ( logLevel LogLevel ) error {
if logLevel . String ( ) != "" {
if err := c . tf . SetLog ( logLevel . String ( ) ) ; err != nil {
return fmt . Errorf ( "set log level %s: %w" , logLevel . String ( ) , err )
}
2023-08-04 07:53:51 -04:00
// Terraform writes its log to the working directory.
// => Set the log path to the parent directory to have it in the user's working directory.
2023-04-14 08:15:07 -04:00
if err := c . tf . SetLogPath ( filepath . Join ( ".." , constants . TerraformLogFile ) ) ; err != nil {
return fmt . Errorf ( "set log path: %w" , err )
}
}
return nil
}
2023-06-27 07:12:50 -04:00
// StateMigration is a manual state migration that is not handled by Terraform due to missing features.
type StateMigration struct {
DisplayName string
Hook func ( ctx context . Context , tfClient TFMigrator ) error
}
2023-08-21 04:26:53 -04:00
// IAMOutput contains the output information of the Terraform IAM operations.
type IAMOutput struct {
GCP GCPIAMOutput
Azure AzureIAMOutput
AWS AWSIAMOutput
}
// GCPIAMOutput contains the output information of the Terraform IAM operation on GCP.
type GCPIAMOutput struct {
SaKey string
}
// AzureIAMOutput contains the output information of the Terraform IAM operation on Microsoft Azure.
type AzureIAMOutput struct {
SubscriptionID string
TenantID string
UAMIID string
}
// AWSIAMOutput contains the output information of the Terraform IAM operation on GCP.
type AWSIAMOutput struct {
ControlPlaneInstanceProfile string
WorkerNodeInstanceProfile string
}
// 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
}
constrainedVersions := & releases . Versions {
Product : product . Terraform ,
Constraints : version ,
}
installCandidates , err := constrainedVersions . List ( ctx )
if err != nil {
return nil , nil , err
}
if len ( installCandidates ) == 0 {
return nil , nil , fmt . Errorf ( "no Terraform version found for constraint %s" , version )
}
downloadVersion := installCandidates [ len ( installCandidates ) - 1 ]
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
}
2023-07-21 10:43:51 -04:00
func toStringSlice ( in [ ] any ) ( [ ] string , error ) {
out := make ( [ ] string , len ( in ) )
for i , v := range in {
s , ok := v . ( string )
if ! ok {
return nil , fmt . Errorf ( "invalid type in list: item at index %v of list is not a string" , i )
}
out [ i ] = s
}
return out , 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 )
2023-05-22 07:31:20 -04:00
Plan ( ctx context . Context , opts ... tfexec . PlanOption ) ( bool , error )
ShowPlanFileRaw ( ctx context . Context , planPath string , opts ... tfexec . ShowOption ) ( string , error )
2023-04-14 08:15:07 -04:00
SetLog ( level string ) error
SetLogPath ( path string ) error
2023-06-27 07:12:50 -04:00
TFMigrator
}
// TFMigrator is an interface for manual terraform state migrations (terraform state mv).
type TFMigrator interface {
StateMv ( ctx context . Context , src , dst string , opts ... tfexec . StateMvCmdOption ) error
2022-09-26 09:52:31 -04:00
}