2023-10-24 09:39:18 -04:00
/ *
Copyright ( c ) Edgeless Systems GmbH
SPDX - License - Identifier : AGPL - 3.0 - only
* /
package cmd
import (
2023-11-20 05:17:16 -05:00
"errors"
2023-10-24 09:39:18 -04:00
"fmt"
2023-11-20 05:17:16 -05:00
"io"
2023-10-24 09:39:18 -04:00
"path/filepath"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
2023-11-20 05:17:16 -05:00
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
2023-10-24 09:39:18 -04:00
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
2023-11-22 08:52:56 -05:00
"github.com/edgelesssys/constellation/v2/internal/state"
2023-10-24 09:39:18 -04:00
"github.com/spf13/cobra"
)
// runTerraformApply checks if changes to Terraform are required and applies them.
func ( a * applyCmd ) runTerraformApply ( cmd * cobra . Command , conf * config . Config , stateFile * state . State , upgradeDir string ) error {
a . log . Debugf ( "Checking if Terraform migrations are required" )
2023-11-20 05:17:16 -05:00
terraformClient , removeClient , err := a . newInfraApplier ( cmd . Context ( ) )
2023-10-30 07:43:38 -04:00
if err != nil {
return fmt . Errorf ( "creating Terraform client: %w" , err )
}
2023-11-20 05:17:16 -05:00
defer removeClient ( )
2023-10-30 07:43:38 -04:00
2023-11-20 05:17:16 -05:00
// Check if we are creating a new cluster by checking if the Terraform workspace is empty
isNewCluster , err := terraformClient . WorkingDirIsEmpty ( )
2023-10-24 09:39:18 -04:00
if err != nil {
2023-11-20 05:17:16 -05:00
return fmt . Errorf ( "checking if Terraform workspace is empty: %w" , err )
2023-10-24 09:39:18 -04:00
}
2023-11-20 05:17:16 -05:00
if changesRequired , err := a . planTerraformChanges ( cmd , conf , terraformClient ) ; err != nil {
return fmt . Errorf ( "planning Terraform migrations: %w" , err )
} else if ! changesRequired {
2023-10-24 09:39:18 -04:00
a . log . Debugf ( "No changes to infrastructure required, skipping Terraform migrations" )
return nil
}
2023-11-20 05:17:16 -05:00
a . log . Debugf ( "Apply new Terraform resources for infrastructure changes" )
newInfraState , err := a . applyTerraformChanges ( cmd , conf , terraformClient , upgradeDir , isNewCluster )
2023-10-24 09:39:18 -04:00
if err != nil {
2023-11-20 05:17:16 -05:00
return err
2023-10-24 09:39:18 -04:00
}
2023-11-20 05:17:16 -05:00
// Merge the original state with the new infrastructure values
2023-10-24 09:39:18 -04:00
a . log . Debugf ( "Updating state file with new infrastructure state" )
if _ , err := stateFile . Merge (
2023-11-20 05:17:16 -05:00
// temporary state with new infrastructure values
state . New ( ) . SetInfrastructure ( newInfraState ) ,
2023-10-24 09:39:18 -04:00
) ; err != nil {
2023-11-20 05:17:16 -05:00
return fmt . Errorf ( "merging old state with new infrastructure values: %w" , err )
2023-10-24 09:39:18 -04:00
}
2023-11-20 05:17:16 -05:00
// Write the new state to disk
2023-10-24 09:39:18 -04:00
if err := stateFile . WriteToFile ( a . fileHandler , constants . StateFilename ) ; err != nil {
return fmt . Errorf ( "writing state file: %w" , err )
}
return nil
}
2023-11-20 05:17:16 -05:00
// planTerraformChanges checks if any changes to the Terraform state are required.
// If no state exists, this function will return true and the caller should create a new state.
func ( a * applyCmd ) planTerraformChanges ( cmd * cobra . Command , conf * config . Config , terraformClient cloudApplier ) ( bool , error ) {
a . log . Debugf ( "Planning Terraform changes" )
2023-10-24 09:39:18 -04:00
2023-11-20 05:17:16 -05:00
// Check if there are any Terraform changes to apply
2023-10-24 09:39:18 -04:00
// Add manual migrations here if required
//
// var manualMigrations []terraform.StateMigration
// for _, migration := range manualMigrations {
// u.log.Debugf("Adding manual Terraform migration: %s", migration.DisplayName)
2023-10-31 07:46:40 -04:00
// u.infraApplier.AddManualStateMigration(migration)
2023-10-24 09:39:18 -04:00
// }
2023-10-30 04:19:35 -04:00
a . spinner . Start ( "Checking for infrastructure changes" , false )
defer a . spinner . Stop ( )
2023-10-31 07:46:40 -04:00
return terraformClient . Plan ( cmd . Context ( ) , conf )
2023-10-24 09:39:18 -04:00
}
2023-11-20 05:17:16 -05:00
// applyTerraformChanges applies planned changes to a Terraform state and returns the resulting infrastructure state.
// If no state existed prior to this function call, a new cluster will be created.
func ( a * applyCmd ) applyTerraformChanges (
cmd * cobra . Command , conf * config . Config , terraformClient cloudApplier , upgradeDir string , isNewCluster bool ,
) ( state . Infrastructure , error ) {
if isNewCluster {
if err := printCreateInfo ( cmd . OutOrStdout ( ) , conf , a . log ) ; err != nil {
return state . Infrastructure { } , err
}
return a . applyTerraformChangesWithMessage (
cmd , conf . GetProvider ( ) , cloudcmd . WithRollbackOnError , terraformClient , upgradeDir ,
"Do you want to create this cluster?" ,
"The creation of the cluster was aborted." ,
"cluster creation aborted by user" ,
"Creating" ,
"Cloud infrastructure created successfully." ,
)
}
cmd . Println ( "Changes of Constellation cloud resources are required by applying an updated Terraform template." )
return a . applyTerraformChangesWithMessage (
cmd , conf . GetProvider ( ) , cloudcmd . WithoutRollbackOnError , terraformClient , upgradeDir ,
"Do you want to apply these Terraform changes?" ,
"Aborting upgrade." ,
"cluster upgrade aborted by user" ,
"Applying Terraform changes" ,
fmt . Sprintf ( "Infrastructure migrations applied successfully and output written to: %s\n" +
"A backup of the pre-upgrade state has been written to: %s" ,
a . flags . pathPrefixer . PrefixPrintablePath ( constants . StateFilename ) ,
a . flags . pathPrefixer . PrefixPrintablePath ( filepath . Join ( upgradeDir , constants . TerraformUpgradeBackupDir ) ) ,
) ,
)
}
func ( a * applyCmd ) applyTerraformChangesWithMessage (
cmd * cobra . Command , csp cloudprovider . Provider , rollbackBehavior cloudcmd . RollbackBehavior ,
terraformClient cloudApplier , upgradeDir string ,
confirmationQst , abortMsg , abortErrorMsg , progressMsg , successMsg string ,
) ( state . Infrastructure , error ) {
2023-10-24 09:39:18 -04:00
// Ask for confirmation first
if ! a . flags . yes {
2023-11-20 05:17:16 -05:00
ok , err := askToConfirm ( cmd , confirmationQst )
2023-10-24 09:39:18 -04:00
if err != nil {
return state . Infrastructure { } , fmt . Errorf ( "asking for confirmation: %w" , err )
}
if ! ok {
2023-11-20 05:17:16 -05:00
cmd . Println ( abortMsg )
// User doesn't expect to see any changes in their workspace after aborting an "apply",
// therefore, restore the workspace to the previous state.
2023-10-31 07:46:40 -04:00
if err := terraformClient . RestoreWorkspace ( ) ; err != nil {
2023-10-24 09:39:18 -04:00
return state . Infrastructure { } , fmt . Errorf (
2023-11-20 05:17:16 -05:00
"restoring Terraform workspace: %w, clean up or restore the Terraform workspace manually from %s " ,
2023-10-24 09:39:18 -04:00
err ,
filepath . Join ( upgradeDir , constants . TerraformUpgradeBackupDir ) ,
)
}
2023-11-20 05:17:16 -05:00
return state . Infrastructure { } , errors . New ( abortErrorMsg )
2023-10-24 09:39:18 -04:00
}
}
2023-11-20 05:17:16 -05:00
a . log . Debugf ( "Applying Terraform changes" )
2023-10-24 09:39:18 -04:00
2023-11-20 05:17:16 -05:00
a . spinner . Start ( progressMsg , false )
infraState , err := terraformClient . Apply ( cmd . Context ( ) , csp , rollbackBehavior )
2023-10-24 09:39:18 -04:00
a . spinner . Stop ( )
if err != nil {
2023-11-20 05:17:16 -05:00
return state . Infrastructure { } , fmt . Errorf ( "applying terraform changes: %w" , err )
2023-10-24 09:39:18 -04:00
}
2023-11-20 05:17:16 -05:00
cmd . Println ( successMsg )
2023-10-24 09:39:18 -04:00
return infraState , nil
}
2023-11-20 05:17:16 -05:00
func printCreateInfo ( out io . Writer , conf * config . Config , log debugLog ) error {
controlPlaneGroup , ok := conf . NodeGroups [ constants . DefaultControlPlaneGroupName ]
if ! ok {
return fmt . Errorf ( "default control-plane node group %q not found in configuration" , constants . DefaultControlPlaneGroupName )
}
controlPlaneType := controlPlaneGroup . InstanceType
workerGroup , ok := conf . NodeGroups [ constants . DefaultWorkerGroupName ]
if ! ok {
return fmt . Errorf ( "default worker node group %q not found in configuration" , constants . DefaultWorkerGroupName )
}
workerGroupType := workerGroup . InstanceType
var qemuInstanceType string
if conf . GetProvider ( ) == cloudprovider . QEMU {
qemuInstanceType = fmt . Sprintf ( "%d-vCPUs" , conf . Provider . QEMU . VCPUs )
controlPlaneType = qemuInstanceType
workerGroupType = qemuInstanceType
}
otherGroupNames := make ( [ ] string , 0 , len ( conf . NodeGroups ) - 2 )
for groupName := range conf . NodeGroups {
if groupName != constants . DefaultControlPlaneGroupName && groupName != constants . DefaultWorkerGroupName {
otherGroupNames = append ( otherGroupNames , groupName )
}
}
if len ( otherGroupNames ) > 0 {
log . Debugf ( "Creating %d additional node groups: %v" , len ( otherGroupNames ) , otherGroupNames )
}
fmt . Fprintf ( out , "The following Constellation cluster will be created:\n" )
fmt . Fprintf ( out , " %d control-plane node%s of type %s will be created.\n" , controlPlaneGroup . InitialCount , isPlural ( controlPlaneGroup . InitialCount ) , controlPlaneType )
fmt . Fprintf ( out , " %d worker node%s of type %s will be created.\n" , workerGroup . InitialCount , isPlural ( workerGroup . InitialCount ) , workerGroupType )
for _ , groupName := range otherGroupNames {
group := conf . NodeGroups [ groupName ]
groupInstanceType := group . InstanceType
if conf . GetProvider ( ) == cloudprovider . QEMU {
groupInstanceType = qemuInstanceType
}
fmt . Fprintf ( out , " group %s with %d node%s of type %s will be created.\n" , groupName , group . InitialCount , isPlural ( group . InitialCount ) , groupInstanceType )
}
return nil
}